Compare commits
54 Commits
plugin-ref
...
main
Author | SHA1 | Date | |
---|---|---|---|
c0b2ce44ad | |||
21f4e7765b | |||
33ba944f8a | |||
05051878f2 | |||
3fff9c7dd0 | |||
eda7992c23 | |||
457429f7ec | |||
8f82360cc7 | |||
8396da2e9a | |||
5c7d07afb0 | |||
52d2507891 | |||
7d677d27c6 | |||
0f85244681 | |||
d59bf264b0 | |||
9b339c738e | |||
165d97245d | |||
a1ffc51a57 | |||
6464b9f8f3 | |||
30d2ccba76 | |||
7d61f76b94 | |||
38b83ac8ac | |||
f8efd45076 | |||
c1f618ad19 | |||
5d49b9ab2d | |||
bb3d75521e | |||
bc1f0fb943 | |||
e95bd759c3 | |||
2918f87911 | |||
33a86882ac | |||
d95ee39c0b | |||
9109e9a4c7 | |||
67a693b916 | |||
b8373f6908 | |||
b14a2e7117 | |||
207345b54b | |||
dc8327289f | |||
a88f8a1f11 | |||
f676f236b1 | |||
4f322e56c7 | |||
4b0d1df34f | |||
128ae21088 | |||
6301bd438a | |||
3f33035611 | |||
32b7a3c01c | |||
91431fd996 | |||
5ccb40bb44 | |||
d210c6ac7c | |||
aa1c2422ef | |||
5698b504e9 | |||
e2b2c82ff7 | |||
7badcc333b | |||
bf27a344e1 | |||
577d61ed42 | |||
777b0fccc8 |
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@ -0,0 +1,19 @@
|
||||
# directories
|
||||
**/bin/
|
||||
**/obj/
|
||||
**/out/
|
||||
.git
|
||||
.idea
|
||||
docs
|
||||
.gitignore
|
||||
.dockerignore
|
||||
MycroForge.sln.DotSettings.user
|
||||
nuget.docker-compose.yml
|
||||
README.md
|
||||
|
||||
# files
|
||||
Dockerfile*
|
||||
**/*.md
|
||||
|
||||
#MycroForge.PluginTemplate
|
||||
#MycroForge.PluginTemplate.Package
|
31
.gitea/workflows/build_cli.yml
Normal file
31
.gitea/workflows/build_cli.yml
Normal file
@ -0,0 +1,31 @@
|
||||
name: Build and publish MycroForge.CLI
|
||||
run-name: ${{ gitea.actor }} triggered a build for the MycroForge.CLI package
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Build and publish NuGet package"
|
||||
run: |
|
||||
# Build the NuGet package
|
||||
cd MycroForge.CLI
|
||||
dotnet pack -v m
|
||||
|
||||
# Add the NuGet source
|
||||
dotnet nuget add source --name devdisciples \
|
||||
--username ${{ secrets.NUGET_USER }} \
|
||||
--password ${{ secrets.NUGET_PASS }} \
|
||||
--store-password-in-clear-text \
|
||||
https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
|
||||
# Get the path to the package
|
||||
PACKAGE="nupkg/$(ls nupkg)"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
33
.gitea/workflows/build_core.yml
Normal file
33
.gitea/workflows/build_core.yml
Normal file
@ -0,0 +1,33 @@
|
||||
name: Build and publish MycroForge.Core
|
||||
run-name: ${{ gitea.actor }} triggered a build for the MycroForge.Core package
|
||||
on: [ workflow_dispatch ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Build and publish NuGet package"
|
||||
run: |
|
||||
# Build the NuGet package
|
||||
cd MycroForge.Core
|
||||
dotnet publish
|
||||
dotnet pack -v m
|
||||
|
||||
# Add the NuGet source
|
||||
dotnet nuget add source --name devdisciples \
|
||||
--username ${{ secrets.NUGET_USER }} \
|
||||
--password ${{ secrets.NUGET_PASS }} \
|
||||
--store-password-in-clear-text \
|
||||
https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
|
||||
# Set the path to the package
|
||||
VERSION=$(grep '<Version>' < MycroForge.Core.csproj | sed 's/.*<Version>\(.*\)<\/Version>/\1/' | xargs)
|
||||
PACKAGE="bin/Release/MycroForge.Core.$VERSION.nupkg"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
33
.gitea/workflows/build_plugin_template.yml
Normal file
33
.gitea/workflows/build_plugin_template.yml
Normal file
@ -0,0 +1,33 @@
|
||||
name: Build and publish MycroForge.PluginTemplate package
|
||||
run-name: ${{ gitea.actor }} triggered a build for the MycroForge.PluginTemplate package
|
||||
on: [ workflow_dispatch ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Build and publish NuGet package"
|
||||
run: |
|
||||
# Build the NuGet package
|
||||
cd MycroForge.PluginTemplate.Package
|
||||
dotnet publish
|
||||
dotnet pack -v m
|
||||
|
||||
# Add the NuGet source
|
||||
dotnet nuget add source --name devdisciples \
|
||||
--username ${{ secrets.NUGET_USER }} \
|
||||
--password ${{ secrets.NUGET_PASS }} \
|
||||
--store-password-in-clear-text \
|
||||
https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
|
||||
# Set the path to the package
|
||||
VERSION=$(grep '<PackageVersion>' < MycroForge.PluginTemplate.Package.csproj | sed 's/.*<PackageVersion>\(.*\)<\/PackageVersion>/\1/' | xargs)
|
||||
PACKAGE="bin/Release/MycroForge.PluginTemplate.Package.$VERSION.nupkg"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
20
.gitea/workflows/test.yml
Normal file
20
.gitea/workflows/test.yml
Normal file
@ -0,0 +1,20 @@
|
||||
name: Test MycroForge.CLI
|
||||
run-name: ${{ gitea.actor }} triggered a test for the MycroForge.CLI
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Reference MycroForge.Core in MycroForge.PluginTemplate"
|
||||
# The MycroForge.PluginTemplate project references MycroForge.Core as a package and not as a reference.
|
||||
# This allows the 'm4g plugin init' command to pull in the core package from a package repository.
|
||||
# To prevent the test command from trying to pull from the package repository, we reference the local project.
|
||||
run: dotnet add MycroForge.PluginTemplate reference MycroForge.Core
|
||||
- name: "Run MycroForge.CLI.Tests"
|
||||
run: dotnet test
|
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@ -0,0 +1,30 @@
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
|
||||
|
||||
# Copy the files to the /tool directory.
|
||||
WORKDIR /tool
|
||||
COPY . .
|
||||
# Manually add a reference to MycroForge.Core in the MycroForge.PluginTemplate project,
|
||||
# otherwise it will try to fetch it from a nuget repository.
|
||||
RUN dotnet add MycroForge.PluginTemplate reference MycroForge.Core
|
||||
|
||||
# Install the tools required for testing
|
||||
RUN apt update -y && \
|
||||
apt upgrade -y && \
|
||||
apt install -y git && \
|
||||
apt install -y bash && \
|
||||
apt install -y python3 && \
|
||||
apt install -y python3-pip && \
|
||||
apt install -y python3-venv
|
||||
|
||||
# Publish the CLI as a global tool and add the .dotnet/tools folder the the PATH variable.
|
||||
WORKDIR /tool/MycroForge.CLI
|
||||
RUN ./scripts/publish-tool.sh
|
||||
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||
|
||||
WORKDIR /test
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||
|
||||
CMD ["sleep", "infinity"]
|
@ -0,0 +1,11 @@
|
||||
using DotNet.Testcontainers.Containers;
|
||||
|
||||
namespace MycroForge.CLI.Tests.Extensions;
|
||||
|
||||
internal static class ContainerInterfaceExtensions
|
||||
{
|
||||
public static Task<byte[]> ReadFileFromRootAsync(this IContainer container, string file)
|
||||
{
|
||||
return container.ReadFileAsync($"/test/todo/{file}");
|
||||
}
|
||||
}
|
1
MycroForge.CLI.Tests/GlobalUsings.cs
Normal file
1
MycroForge.CLI.Tests/GlobalUsings.cs
Normal file
@ -0,0 +1 @@
|
||||
global using NUnit.Framework;
|
222
MycroForge.CLI.Tests/InitCommandTests.cs
Normal file
222
MycroForge.CLI.Tests/InitCommandTests.cs
Normal file
@ -0,0 +1,222 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DotNet.Testcontainers.Builders;
|
||||
using DotNet.Testcontainers.Containers;
|
||||
using DotNet.Testcontainers.Images;
|
||||
using MycroForge.CLI.Tests.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Tests;
|
||||
|
||||
public class InitCommandTests
|
||||
{
|
||||
private IFutureDockerImage Image = null!;
|
||||
private IContainer Container = null!;
|
||||
|
||||
private async Task CreateImage()
|
||||
{
|
||||
Image = new ImageFromDockerfileBuilder()
|
||||
.WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), string.Empty)
|
||||
.WithDockerfile("Dockerfile")
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
await Image.CreateAsync();
|
||||
}
|
||||
|
||||
private async Task CreateContainer()
|
||||
{
|
||||
Container = new ContainerBuilder()
|
||||
.WithImage(Image)
|
||||
.WithCleanUp(true)
|
||||
.Build();
|
||||
|
||||
await Container.StartAsync();
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
public async Task SetupOnce()
|
||||
{
|
||||
await CreateImage();
|
||||
|
||||
await CreateContainer();
|
||||
|
||||
await Container.ExecAsync(["m4g", "init", "todo"]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_m4g_json()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("m4g.json");
|
||||
using var stream = new MemoryStream(bytes);
|
||||
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(stream, DefaultJsonSerializerOptions.Default);
|
||||
|
||||
Assert.That(config, Is.Not.Null);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(config!.Api, Is.Not.Null);
|
||||
Assert.That(config.Api.Port, Is.EqualTo(8000));
|
||||
Assert.That(config.Db, Is.Not.Null);
|
||||
Assert.That(config.Db.DbhPort, Is.EqualTo(5050));
|
||||
Assert.That(config.Db.DbuPort, Is.EqualTo(5051));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_main_py()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("main.py");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain("from fastapi import FastAPI"));
|
||||
Assert.That(lines, Does.Contain("from api.routers import hello"));
|
||||
Assert.That(lines, Does.Contain("app = FastAPI()"));
|
||||
Assert.That(lines, Does.Contain("app.include_router(prefix=\"/hello\", router=hello.router)"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_gitignore()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync(".gitignore");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain(".venv"));
|
||||
Assert.That(lines, Does.Contain("__pycache__/"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_alembic_ini()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("alembic.ini");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain("[alembic]"));
|
||||
Assert.That(lines, Does.Contain("sqlalchemy.url = driver://user:pass@localhost/dbname"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_db_docker_compose_yml()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("db.docker-compose.yml");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain(" todo_mariadb:"));
|
||||
Assert.That(lines, Does.Contain(" container_name: 'todo_mariadb'"));
|
||||
Assert.That(lines, Does.Contain(" - '${DBH_PORT}:3306'"));
|
||||
Assert.That(lines, Does.Contain(" MYSQL_DATABASE: 'todo'"));
|
||||
Assert.That(lines, Does.Contain(" - 'todo_mariadb:/var/lib/mysql'"));
|
||||
Assert.That(lines, Does.Contain(" todo_phpmyadmin:"));
|
||||
Assert.That(lines, Does.Contain(" container_name: todo_phpmyadmin"));
|
||||
Assert.That(lines, Does.Contain(" PMA_HOST: todo_mariadb"));
|
||||
Assert.That(lines, Does.Contain(" - todo_mariadb"));
|
||||
Assert.That(lines, Does.Contain(" todo_mariadb:"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_api__router_hello_py()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("api/routers/hello.py");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain("from fastapi import APIRouter"));
|
||||
Assert.That(lines, Does.Contain("from fastapi.responses import JSONResponse"));
|
||||
Assert.That(lines, Does.Contain("from fastapi.encoders import jsonable_encoder"));
|
||||
Assert.That(lines, Does.Contain("router = APIRouter()"));
|
||||
Assert.That(lines, Does.Contain("@router.get(\"/{name}\")"));
|
||||
Assert.That(lines, Does.Contain("async def hello(name: str):"));
|
||||
Assert.That(lines, Does.Contain("\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_db__engine__async_session_py()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("db/engine/async_session.py");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain("from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession"));
|
||||
Assert.That(lines, Does.Contain("from db.settings import DbSettings"));
|
||||
Assert.That(lines, Does.Contain("async_engine: AsyncEngine = create_async_engine(DbSettings.get_connectionstring())"));
|
||||
Assert.That(lines, Does.Contain("def async_session() -> AsyncSession:"));
|
||||
Assert.That(lines, Does.Contain("\treturn AsyncSession(async_engine, expire_on_commit=False)"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_db__entities__entity_base_py()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("db/entities/entity_base.py");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain("from sqlalchemy.orm import DeclarativeBase"));
|
||||
Assert.That(lines, Does.Contain("class EntityBase(DeclarativeBase):"));
|
||||
Assert.That(lines, Does.Contain("\tpass"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Creates_db__settings_py()
|
||||
{
|
||||
var bytes = await Container.ReadFileFromRootAsync("db/settings.py");
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
var lines = text.Split('\n');
|
||||
|
||||
// TestContext.WriteLine(text);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(text, Is.Not.Empty);
|
||||
Assert.That(lines, Does.Contain("class DbSettings:"));
|
||||
Assert.That(lines, Does.Contain("\tdef get_connectionstring() -> str:"));
|
||||
Assert.That(lines, Does.Contain("\t\tconnectionstring = \"mysql+asyncmy://root:password@localhost:5050/todo\""));
|
||||
Assert.That(lines, Does.Contain("\t\treturn connectionstring"));
|
||||
});
|
||||
}
|
||||
}
|
25
MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj
Normal file
25
MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj
Normal file
@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||
<PackageReference Include="NUnit" Version="3.13.3"/>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||
<PackageReference Include="Testcontainers" Version="3.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MycroForge.Core\MycroForge.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -1,4 +1,5 @@
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
@ -6,6 +7,8 @@ namespace MycroForge.CLI.CodeGen;
|
||||
|
||||
public class CrudRouterGenerator
|
||||
{
|
||||
#region Templates
|
||||
|
||||
private static readonly string[] Template =
|
||||
[
|
||||
"from typing import Annotated",
|
||||
@ -79,6 +82,8 @@ public class CrudRouterGenerator
|
||||
"\t\treturn JSONResponse(status_code=500, content=str(ex))",
|
||||
];
|
||||
|
||||
#endregion
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public CrudRouterGenerator(ProjectContext context)
|
||||
@ -86,65 +91,49 @@ public class CrudRouterGenerator
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Generate(string path, string entity)
|
||||
public async Task Generate(FullyQualifiedName fqn)
|
||||
{
|
||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
||||
var entityClassName = entity.Pascalize();
|
||||
var serviceClassName = $"{entityClassName}Service";
|
||||
var entityRoutePrefix = entity.Kebaberize().Pluralize().ToLower();
|
||||
var serviceClassName = $"{fqn.PascalizedName}Service";
|
||||
var entityRoutePrefix = fqn.PascalizedName.Kebaberize().Pluralize().ToLower();
|
||||
|
||||
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
|
||||
var serviceFilePath = $"{servicesFolderPath}/{entitySnakeCaseName}_service.py";
|
||||
var serviceImportPath = serviceFilePath
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Replace(".py", string.Empty)
|
||||
.DeduplicateDots()
|
||||
.Trim();
|
||||
var serviceFilePath = Path.Join(
|
||||
Features.Api.FeatureName, "services", fqn.Namespace, $"{fqn.SnakeCasedName}_service"
|
||||
);
|
||||
|
||||
var serviceImportPath = serviceFilePath.SlashesToDots();
|
||||
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.Namespace);
|
||||
var routerFilePath = Path.Join(routerFolderPath, $"{fqn.SnakeCasedName}");
|
||||
var routerImportPath = routerFolderPath.SlashesToDots();
|
||||
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.Namespace);
|
||||
|
||||
var routersFolderPath = $"{Features.Api.FeatureName}/routers/{path}";
|
||||
var routerFilePath = $"{routersFolderPath}/{entitySnakeCaseName}.py";
|
||||
var routerImportPath = routersFolderPath
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Replace(".py", "")
|
||||
.DeduplicateDots()
|
||||
.Trim();
|
||||
|
||||
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
|
||||
|
||||
var createRequestImportPath = $"{requestsFolderPath}/Create{entityClassName}Request"
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.DeduplicateDots()
|
||||
var createRequestImportPath = Path.Join(requestsFolderPath, $"Create{fqn.PascalizedName}Request")
|
||||
.SlashesToDots()
|
||||
.Underscore()
|
||||
.ToLower();
|
||||
var createRequestClassName = $"Create{entityClassName}Request";
|
||||
var createRequestClassName = $"Create{fqn.PascalizedName}Request";
|
||||
|
||||
var updateRequestImportPath = $"{requestsFolderPath}/Update{entityClassName}Request"
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.DeduplicateDots()
|
||||
var updateRequestImportPath = Path.Join(requestsFolderPath, $"Update{fqn.PascalizedName}Request")
|
||||
.SlashesToDots()
|
||||
.Underscore()
|
||||
.ToLower();
|
||||
var updateRequestClassName = $"Update{entityClassName}Request";
|
||||
var updateRequestClassName = $"Update{fqn.PascalizedName}Request";
|
||||
|
||||
var router = string.Join("\n", Template)
|
||||
.Replace("%service_import_path%", serviceImportPath)
|
||||
.Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
.Replace("%service_class_name%", serviceClassName)
|
||||
.Replace("%create_entity_request_import_path%", createRequestImportPath)
|
||||
.Replace("%create_entity_request_class_name%", createRequestClassName)
|
||||
.Replace("%update_entity_request_import_path%", updateRequestImportPath)
|
||||
.Replace("%update_entity_request_class_name%", updateRequestClassName);
|
||||
|
||||
await _context.CreateFile(routerFilePath, router);
|
||||
await _context.CreateFile($"{routerFilePath}.py", router);
|
||||
|
||||
var main = await _context.ReadFile("main.py");
|
||||
|
||||
main = new MainModifier(main).Initialize()
|
||||
.Import(from: routerImportPath, import: entitySnakeCaseName)
|
||||
.IncludeRouter(prefix: entityRoutePrefix, router: entitySnakeCaseName)
|
||||
.Import(from: routerImportPath, import: fqn.SnakeCasedName)
|
||||
.IncludeRouter(prefix: entityRoutePrefix, router: fqn.SnakeCasedName)
|
||||
.Rewrite();
|
||||
|
||||
await _context.WriteFile("main.py", main);
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.CodeGen;
|
||||
@ -65,26 +64,15 @@ public class CrudServiceGenerator
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Generate(string path, string entity)
|
||||
public async Task Generate(FullyQualifiedName fqn)
|
||||
{
|
||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
||||
var entityClassName = entity.Pascalize();
|
||||
var entityImportPath = fqn.GetImportPath(root: [Features.Db.FeatureName, "entities"]);
|
||||
|
||||
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
|
||||
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
|
||||
var entityImportPath = entityFilePath
|
||||
.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Replace(".py", string.Empty)
|
||||
.DeduplicateDots()
|
||||
.Trim();
|
||||
|
||||
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
|
||||
var serviceFilePath = $"{servicesFolderPath}/{entity.Underscore().ToLower()}_service.py";
|
||||
var serviceFilePath = Path.Join(Features.Api.FeatureName, "services", $"{fqn.FilePath}_service.py");
|
||||
|
||||
var service = string.Join("\n", Template)
|
||||
.Replace("%entity_import_path%", entityImportPath)
|
||||
.Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
;
|
||||
|
||||
await _context.CreateFile(serviceFilePath, service);
|
||||
|
@ -96,14 +96,18 @@ public partial class EntityLinker
|
||||
var left = await LoadEntity(_left);
|
||||
var right = await LoadEntity(_right);
|
||||
|
||||
var associationTable = string.Join('\n', AssociationTable);
|
||||
associationTable = associationTable
|
||||
var associationTable = string.Join('\n', AssociationTable)
|
||||
.Replace("%left_entity%", left.ClassName.Underscore().ToLower())
|
||||
.Replace("%right_entity%", right.ClassName.Underscore().ToLower())
|
||||
.Replace("%left_table%", left.TableName)
|
||||
.Replace("%right_table%", right.TableName);
|
||||
var associationTablePath =
|
||||
$"{Features.Db.FeatureName}/entities/associations/{left.TableName.Singularize()}_{right.TableName.Singularize()}_mapping.py";
|
||||
|
||||
var associationTablePath = Path.Join(
|
||||
Features.Db.FeatureName,
|
||||
"entities",
|
||||
"associations",
|
||||
$"{left.TableName.Singularize()}_{right.TableName.Singularize()}_mapping.py"
|
||||
);
|
||||
|
||||
await _context.CreateFile(associationTablePath, associationTable);
|
||||
|
||||
@ -136,21 +140,25 @@ public partial class EntityLinker
|
||||
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
|
||||
env = new DbEnvModifier(env, associationTableImportPath, associationTableImportName).Rewrite();
|
||||
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
|
||||
|
||||
|
||||
var main = await _context.ReadFile("main.py");
|
||||
main = new MainModifier(main).Initialize().Import(associationTableImportPath, associationTableImportName).Rewrite();
|
||||
main = new MainModifier(main)
|
||||
.Initialize()
|
||||
.Import(associationTableImportPath, associationTableImportName)
|
||||
.Rewrite();
|
||||
|
||||
await _context.WriteFile("main.py", main);
|
||||
}
|
||||
|
||||
private async Task<EntityModel> LoadEntity(string name)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var path = $"{Features.Db.FeatureName}/entities";
|
||||
var path = Path.Join(Features.Db.FeatureName, "entities");
|
||||
|
||||
if (fqn.HasPath)
|
||||
path = Path.Combine(path, fqn.Path);
|
||||
if (fqn.HasNamespace)
|
||||
path = Path.Join(path, fqn.Namespace);
|
||||
|
||||
path = Path.Combine(path, $"{fqn.SnakeCasedName}.py");
|
||||
path = Path.Join(path, $"{fqn.SnakeCasedName}.py");
|
||||
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));
|
||||
entity.Initialize();
|
||||
return entity;
|
||||
|
@ -38,12 +38,14 @@ public class MainModifier
|
||||
|
||||
public MainModifier IncludeRouter(string prefix, string router)
|
||||
{
|
||||
_routerIncludeBuffer.Add($"\napp.include_router(prefix=\"/{prefix}\", router={router}.router)\n");
|
||||
_routerIncludeBuffer.Add($"app.include_router(prefix=\"/{prefix}\", router={router}.router)");
|
||||
return this;
|
||||
}
|
||||
|
||||
public string Rewrite()
|
||||
{
|
||||
// Make sure to insert the includes before the imports, if done the other way around,
|
||||
// the insertions of the includes will change the indexes of the imports.
|
||||
InsertIncludes();
|
||||
|
||||
InsertImports();
|
||||
@ -54,7 +56,7 @@ public class MainModifier
|
||||
private void InsertImports()
|
||||
{
|
||||
if (_importsBuffer.Count == 0) return;
|
||||
|
||||
|
||||
if (_lastImport is not null)
|
||||
{
|
||||
_source.InsertMultiLine(_lastImport.EndIndex, _importsBuffer.ToArray());
|
||||
@ -69,15 +71,17 @@ public class MainModifier
|
||||
{
|
||||
if (_routerIncludeBuffer.Count == 0) return;
|
||||
|
||||
// Prepend an empty string to the router include buffer,
|
||||
// this will ensure that the new entries are all on separate lines.
|
||||
var content = _routerIncludeBuffer.Prepend(string.Empty).ToArray();
|
||||
|
||||
if (_lastRouterInclude is not null)
|
||||
{
|
||||
_source.InsertMultiLine(
|
||||
_lastRouterInclude.EndIndex, _routerIncludeBuffer.ToArray()
|
||||
);
|
||||
_source.InsertMultiLine(_lastRouterInclude.EndIndex, content);
|
||||
}
|
||||
else
|
||||
{
|
||||
_source.InsertMultiLineAtEnd(_routerIncludeBuffer.ToArray());
|
||||
_source.InsertMultiLineAtEnd(content);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,22 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.CodeGen;
|
||||
|
||||
public class RequestClassGenerator
|
||||
{
|
||||
public record Import(string Name, List<string> Types)
|
||||
private static readonly List<string> PythonTypingImports = ["Any", "Dict", "List", "Optional"];
|
||||
|
||||
private record Import(string Name, List<string> Types)
|
||||
{
|
||||
public bool Match(string type) => Types.Any(t => type == t || type.StartsWith(t));
|
||||
|
||||
public string FindType(string type) => Types.First(t => type == t || type.StartsWith(t));
|
||||
};
|
||||
|
||||
public record Field(string Name, string Type);
|
||||
private record Field(string Name, string Type);
|
||||
|
||||
public enum Type
|
||||
{
|
||||
@ -40,29 +43,28 @@ public class RequestClassGenerator
|
||||
_context = context;
|
||||
}
|
||||
|
||||
public async Task Generate(string path, string entity, Type type)
|
||||
public async Task Generate(FullyQualifiedName fqn, Type type)
|
||||
{
|
||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
||||
var entityClassName = entity.Pascalize();
|
||||
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
|
||||
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
|
||||
var entityFilePath = Path.Join(Features.Db.FeatureName, "entities", $"{fqn.FilePath}.py");
|
||||
var entitySource = await _context.ReadFile(entityFilePath);
|
||||
|
||||
var fieldInfo = ReadFields(entitySource);
|
||||
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
||||
|
||||
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
|
||||
var updateRequestFilePath =
|
||||
$"{requestsFolderPath}/{type.ToString().ToLower()}_{entitySnakeCaseName}_request.py";
|
||||
var requestFilePath = Path.Join(
|
||||
Features.Api.FeatureName,
|
||||
"requests",
|
||||
fqn.Namespace,
|
||||
$"{type.ToString().ToLower()}_{fqn.SnakeCasedName}_request.py"
|
||||
);
|
||||
|
||||
var service = string.Join("\n", Template)
|
||||
.Replace("%imports%", GetImportString(entitySource, fieldInfo, type))
|
||||
.Replace("%request_type%", type.ToString().Pascalize())
|
||||
.Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
.Replace("%fields%", fields)
|
||||
;
|
||||
|
||||
await _context.CreateFile(updateRequestFilePath, service);
|
||||
await _context.CreateFile(requestFilePath, service);
|
||||
}
|
||||
|
||||
private string ToFieldString(Field field, Type type)
|
||||
@ -100,11 +102,12 @@ public class RequestClassGenerator
|
||||
.Replace(" ", "");
|
||||
Console.WriteLine(str); // = "List,Dict,str,Any"
|
||||
*/
|
||||
var dissectedTypes = field.Type.Replace("[", ",")
|
||||
var dissectedTypes = field.Type
|
||||
.Replace("[", ",")
|
||||
.Replace("]", "")
|
||||
.Replace(" ", "")
|
||||
.Split();
|
||||
|
||||
.Split(',');
|
||||
|
||||
foreach (var dissectedType in dissectedTypes)
|
||||
{
|
||||
if (imports.FirstOrDefault(i => i.Match(dissectedType)) is Import import)
|
||||
@ -164,16 +167,16 @@ public class RequestClassGenerator
|
||||
.Split(',')
|
||||
.Select(s => s.Trim())
|
||||
.ToArray();
|
||||
imports.Add(new Import(name, [..types]));
|
||||
imports.Add(new Import(name, new List<string>(types)));
|
||||
}
|
||||
|
||||
if (imports.FirstOrDefault(i => i.Name == "typing") is Import typingImport)
|
||||
{
|
||||
typingImport.Types.AddRange(["Any", "Dict", "List", "Optional"]);
|
||||
typingImport.Types.AddRange(PythonTypingImports);
|
||||
}
|
||||
else
|
||||
{
|
||||
imports.Add(new("typing", ["Any", "Dict", "List", "Optional"]));
|
||||
imports.Add(new Import("typing", PythonTypingImports));
|
||||
}
|
||||
|
||||
return imports;
|
||||
|
@ -0,0 +1,17 @@
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresFeatureAttribute : Attribute
|
||||
{
|
||||
public string FeatureName { get; }
|
||||
|
||||
public RequiresFeatureAttribute(string featureName)
|
||||
{
|
||||
FeatureName = featureName;
|
||||
}
|
||||
|
||||
public void RequireFeature(string command)
|
||||
{
|
||||
if (!Directory.Exists(Path.Join(Environment.CurrentDirectory, FeatureName)))
|
||||
throw new($"Command '{command}' requires feature {FeatureName}");
|
||||
}
|
||||
}
|
17
MycroForge.CLI/Commands/Attributes/RequiresFileAttribute.cs
Normal file
17
MycroForge.CLI/Commands/Attributes/RequiresFileAttribute.cs
Normal file
@ -0,0 +1,17 @@
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresFileAttribute : Attribute
|
||||
{
|
||||
public string FilePath { get; }
|
||||
|
||||
public RequiresFileAttribute(string filePath)
|
||||
{
|
||||
FilePath = filePath;
|
||||
}
|
||||
|
||||
public void RequireFile(string command)
|
||||
{
|
||||
if (!File.Exists(Path.Join(Environment.CurrentDirectory, FilePath)))
|
||||
throw new($"Command '{command}' requires file {FilePath}");
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresPluginAttribute : Attribute
|
||||
{
|
||||
public void RequirePluginProject(string command)
|
||||
{
|
||||
var currentDirectoryInfo = new DirectoryInfo(Environment.CurrentDirectory);
|
||||
var matcher = new Matcher()
|
||||
.AddInclude("*.csproj")
|
||||
.Execute(new DirectoryInfoWrapper(currentDirectoryInfo));
|
||||
|
||||
if (!matcher.HasMatches)
|
||||
throw new($"Command '{command}' must be run in a command plugin project.");
|
||||
|
||||
var csprojFileName = $"{new DirectoryInfo(Environment.CurrentDirectory).Name}.csproj";
|
||||
|
||||
bool IsCsprojFile(FilePatternMatch file)
|
||||
{
|
||||
return Path.GetFileName(file.Path) == csprojFileName;
|
||||
}
|
||||
|
||||
var hasCsprojFile = matcher.Files.Any(IsCsprojFile);
|
||||
|
||||
if (!hasCsprojFile)
|
||||
throw new($"File '{csprojFileName}' was not found, make sure you're in a command plugin project.");
|
||||
}
|
||||
}
|
12
MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs
Normal file
12
MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
public class RequiresVenvAttribute : Attribute
|
||||
{
|
||||
public string Path => System.IO.Path.Join(Environment.CurrentDirectory, ".venv");
|
||||
|
||||
public void RequireVenv(string command)
|
||||
{
|
||||
if (!File.Exists(System.IO.Path.Join(Environment.CurrentDirectory, Path)))
|
||||
throw new($"Command '{command}' requires directory {Path}");
|
||||
}
|
||||
}
|
@ -1,28 +1,48 @@
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.Extensions;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public class FullyQualifiedName
|
||||
{
|
||||
public string Path { get; }
|
||||
public string Namespace { get; }
|
||||
public string PascalizedName { get; }
|
||||
public string SnakeCasedName { get; }
|
||||
|
||||
public bool HasPath => Path.Length > 0;
|
||||
public string FilePath =>
|
||||
string.IsNullOrEmpty(Namespace.Trim())
|
||||
? SnakeCasedName
|
||||
: Path.Join(Namespace, SnakeCasedName);
|
||||
|
||||
public bool HasNamespace => Namespace.Length > 0;
|
||||
|
||||
|
||||
|
||||
public FullyQualifiedName(string name)
|
||||
{
|
||||
var path = string.Empty;
|
||||
|
||||
|
||||
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
|
||||
{
|
||||
path = fullName[0];
|
||||
name = fullName[1];
|
||||
}
|
||||
|
||||
Path = path;
|
||||
|
||||
Namespace = path;
|
||||
PascalizedName = name.Pascalize();
|
||||
SnakeCasedName = name.Underscore().ToLower();
|
||||
SnakeCasedName = SnakeCase(name);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetImportPath(params string[] root)
|
||||
{
|
||||
if (root.Length == 0)
|
||||
return string.Join('.', FilePath).SlashesToDots();
|
||||
|
||||
var importRoot = string.Join('.', root);
|
||||
|
||||
return string.Join('.', SnakeCase(importRoot), FilePath).SlashesToDots();
|
||||
}
|
||||
|
||||
private static string SnakeCase(string value) => value.Underscore().ToLower();
|
||||
|
||||
// private static string SlashesToDots(string value) => value.Replace('\\', '.').Replace('/', '.');
|
||||
}
|
@ -21,6 +21,11 @@ public partial class MycroForge
|
||||
description: "The database UI port"
|
||||
);
|
||||
|
||||
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
|
||||
aliases: ["--database-ui-platform", "--dbu-platform"],
|
||||
description: "The docker platform for the PhpMyAdmin image"
|
||||
);
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
private readonly OptionsContainer _optionsContainer;
|
||||
private readonly List<IFeature> _features;
|
||||
@ -34,12 +39,22 @@ public partial class MycroForge
|
||||
|
||||
AddOption(DbhPortOption);
|
||||
AddOption(DbuPortOption);
|
||||
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption);
|
||||
AddOption(DbuPlatformOption);
|
||||
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption, DbuPlatformOption);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(int dbhPort, int dbuPort)
|
||||
private async Task ExecuteAsync(
|
||||
int dbhPort,
|
||||
int dbuPort,
|
||||
ProjectConfig.DbConfig.DbuPlatformOptions dbuPlatform
|
||||
)
|
||||
{
|
||||
_optionsContainer.Set(new Features.Db.Options { DbhPort = dbhPort, DbuPort = dbuPort });
|
||||
_optionsContainer.Set(new Features.Db.Options
|
||||
{
|
||||
DbhPort = dbhPort,
|
||||
DbuPort = dbuPort,
|
||||
DbuPlatform = dbuPlatform
|
||||
});
|
||||
var feature = _features.First(f => f.Name == Features.Db.FeatureName);
|
||||
await feature.ExecuteAsync(_context);
|
||||
}
|
||||
|
@ -29,11 +29,10 @@ public partial class MycroForge
|
||||
private async Task ExecuteAsync(string entity)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(entity);
|
||||
|
||||
await new CrudServiceGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
|
||||
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Create);
|
||||
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Update);
|
||||
await new CrudRouterGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
|
||||
await new CrudServiceGenerator(_context).Generate(fqn);
|
||||
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Create);
|
||||
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Update);
|
||||
await new CrudRouterGenerator(_context).Generate(fqn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ using System.CommandLine;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.Core.Contract;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
@ -44,19 +43,17 @@ public partial class MycroForge
|
||||
private async Task ExecuteAsync(string name)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var folderPath = $"{Features.Api.FeatureName}/routers";
|
||||
var routersFolderPath = Path.Join(Features.Api.FeatureName, "routers");
|
||||
|
||||
_context.AssertDirectoryExists(folderPath);
|
||||
if (fqn.HasNamespace)
|
||||
routersFolderPath = Path.Join(routersFolderPath, fqn.Namespace);
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
||||
|
||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
var filePath = Path.Join(routersFolderPath, fileName);
|
||||
|
||||
await _context.CreateFile(filePath, Template);
|
||||
|
||||
var moduleImportPath = folderPath
|
||||
|
||||
var moduleImportPath = routersFolderPath
|
||||
.Replace('\\', '.')
|
||||
.Replace('/', '.');
|
||||
|
||||
|
@ -1,17 +1,19 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresFeature(Features.Api.FeatureName)]
|
||||
public partial class Api : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
public Api(IEnumerable<ISubCommandOf<Api>> subCommands) :
|
||||
public Api(IEnumerable<ISubCommandOf<Api>> commands) :
|
||||
base("api", "API related commands")
|
||||
{
|
||||
foreach (var subCommandOf in subCommands)
|
||||
AddCommand((subCommandOf as Command)!);
|
||||
foreach (var command in commands)
|
||||
AddCommand((command as Command)!);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.RegularExpressions;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.Core.Contract;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
@ -15,11 +15,65 @@ public partial class MycroForge
|
||||
{
|
||||
public class Entity : Command, ISubCommandOf<Generate>
|
||||
{
|
||||
private record ColumnDefinition(string Name, string NativeType, string OrmType);
|
||||
#region Hidden region
|
||||
|
||||
private static string[] SqlAlchemyTypes =
|
||||
[
|
||||
"BigInteger",
|
||||
"Boolean",
|
||||
"Date",
|
||||
"DateTime",
|
||||
"Enum",
|
||||
"Double",
|
||||
"Float",
|
||||
"Integer",
|
||||
"Interval",
|
||||
"LargeBinary",
|
||||
"MatchType",
|
||||
"Numeric",
|
||||
"PickleType",
|
||||
"SchemaType",
|
||||
"SmallInteger",
|
||||
"String",
|
||||
"Text",
|
||||
"Time",
|
||||
"Unicode",
|
||||
"UnicodeText",
|
||||
"Uuid",
|
||||
"ARRAY",
|
||||
"BIGINT",
|
||||
"BINARY",
|
||||
"BLOB",
|
||||
"BOOLEAN",
|
||||
"CHAR",
|
||||
"CLOB",
|
||||
"DATE",
|
||||
"DATETIME",
|
||||
"DECIMAL",
|
||||
"DOUBLE",
|
||||
"DOUBLE_PRECISION",
|
||||
"FLOAT",
|
||||
"INT",
|
||||
"JSON",
|
||||
"INTEGER",
|
||||
"NCHAR",
|
||||
"NVARCHAR",
|
||||
"NUMERIC",
|
||||
"REAL",
|
||||
"SMALLINT",
|
||||
"TEXT",
|
||||
"TIME",
|
||||
"TIMESTAMP",
|
||||
"UUID",
|
||||
"VARBINARY",
|
||||
"VARCHAR"
|
||||
];
|
||||
|
||||
private static readonly Regex SqlAlchemyTypeRegex = new(@".*\(.*\)");
|
||||
|
||||
private static readonly string[] Template =
|
||||
[
|
||||
"from sqlalchemy import %type_imports%",
|
||||
"from sqlalchemy import %sqlalchemy_imports%",
|
||||
"from sqlalchemy.orm import Mapped, mapped_column",
|
||||
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
||||
"",
|
||||
@ -56,6 +110,10 @@ public partial class MycroForge
|
||||
"\tfirst_name:str:String(255)",
|
||||
])) { AllowMultipleArgumentsPerToken = true };
|
||||
|
||||
#endregion
|
||||
|
||||
private record ColumnDefinition(string Name, string NativeType, string SqlAlchemyType);
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Entity(ProjectContext context) : base("entity", "Generate and database entity")
|
||||
@ -70,25 +128,27 @@ public partial class MycroForge
|
||||
private async Task ExecuteAsync(string name, IEnumerable<string> columns)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var folderPath = $"{Features.Db.FeatureName}/entities";
|
||||
var folderPath = Path.Join(Features.Db.FeatureName, "entities");
|
||||
|
||||
_context.AssertDirectoryExists(Features.Db.FeatureName);
|
||||
if (fqn.HasNamespace)
|
||||
folderPath = Path.Join(folderPath, fqn.Namespace);
|
||||
|
||||
var sqlAlchemyColumn = GetColumnDefinitions(columns.ToArray());
|
||||
var distinctSqlAlchemyColumnTypes = sqlAlchemyColumn
|
||||
.Select(c => c.SqlAlchemyType.Split('(').First())
|
||||
.Distinct();
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
||||
|
||||
var _columns = GetColumnDefinitions(columns.ToArray());
|
||||
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());
|
||||
var columnDefinitions = string.Join("\n\t", _columns.Select(ColumnToString));
|
||||
var sqlAlchemyImport = string.Join(", ", distinctSqlAlchemyColumnTypes);
|
||||
var columnDefinitions = string.Join("\n ", sqlAlchemyColumn.Select(ColumnToString));
|
||||
|
||||
var code = string.Join('\n', Template);
|
||||
code = code.Replace("%type_imports%", typeImports);
|
||||
code = code.Replace("%sqlalchemy_imports%", sqlAlchemyImport);
|
||||
code = code.Replace("%class_name%", fqn.PascalizedName);
|
||||
code = code.Replace("%table_name%", fqn.SnakeCasedName.Pluralize());
|
||||
code = code.Replace("%column_definitions%", columnDefinitions);
|
||||
|
||||
|
||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
var filePath = Path.Join(folderPath, fileName);
|
||||
await _context.CreateFile(filePath, code);
|
||||
|
||||
var importPathParts = new[] { folderPath, fileName.Replace(".py", "") }
|
||||
@ -109,25 +169,58 @@ public partial class MycroForge
|
||||
await _context.WriteFile("main.py", main);
|
||||
}
|
||||
|
||||
private List<ColumnDefinition> GetColumnDefinitions(string[] fields)
|
||||
private List<ColumnDefinition> GetColumnDefinitions(string[] columns)
|
||||
{
|
||||
var definitions = new List<ColumnDefinition>();
|
||||
|
||||
foreach (var field in fields)
|
||||
foreach (var column in columns)
|
||||
{
|
||||
if (field.Split(':') is not { Length: 3 } definition)
|
||||
throw new Exception($"Field definition {field} is invalid.");
|
||||
if (column.Split(':') is not { Length: 3 } definition)
|
||||
throw new Exception($"Column definition {column} is invalid.");
|
||||
|
||||
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
||||
}
|
||||
|
||||
ValidateSqlAlchemyColumnTypes(definitions);
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private static string ColumnToString(ColumnDefinition definition)
|
||||
private static void ValidateSqlAlchemyColumnTypes(List<ColumnDefinition> definitions)
|
||||
{
|
||||
return $"{definition.Name}: Mapped[{definition.NativeType}] = mapped_column({definition.OrmType})";
|
||||
foreach (var column in definitions)
|
||||
{
|
||||
if (!SqlAlchemyTypeRegex.IsMatch(column.SqlAlchemyType))
|
||||
{
|
||||
var message = new[]
|
||||
{
|
||||
$"SQLAlchemy column definition {column.SqlAlchemyType} was not properly defined.",
|
||||
"Add parentheses and specify parameters if required, an example is provided below.",
|
||||
" String(255)",
|
||||
"",
|
||||
"Available options are:",
|
||||
string.Join(Environment.NewLine, SqlAlchemyTypes.Select(type => $" - {type}"))
|
||||
};
|
||||
|
||||
throw new(string.Join(Environment.NewLine, message));
|
||||
}
|
||||
|
||||
var type = column.SqlAlchemyType.Split('(').First();
|
||||
|
||||
if (!SqlAlchemyTypes.Contains(type))
|
||||
{
|
||||
var message = string.Join(Environment.NewLine, [
|
||||
$"SQLAlchemy column type '{column.SqlAlchemyType}' is not valid, available options are:",
|
||||
string.Join(Environment.NewLine, SqlAlchemyTypes.Select(type => $" - {type}"))
|
||||
]);
|
||||
|
||||
throw new(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ColumnToString(ColumnDefinition definition) =>
|
||||
$"{definition.Name}: Mapped[{definition.NativeType}] = mapped_column({definition.SqlAlchemyType})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync(string name)
|
||||
{
|
||||
_context.AssertDirectoryExists($"{Features.Db.FeatureName}/versions");
|
||||
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
|
||||
|
@ -23,7 +23,8 @@ public partial class MycroForge
|
||||
{
|
||||
var config = await _context.LoadConfig();
|
||||
var env = $"DBH_PORT={config.Db.DbhPort} DBU_PORT={config.Db.DbuPort}";
|
||||
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d");
|
||||
var command = $"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d";
|
||||
await _context.Bash(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,9 +21,10 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync()
|
||||
{
|
||||
var config = await _context.LoadConfig();
|
||||
var env = $"DB_PORT={config.Db.DbhPort} PMA_PORT={config.Db.DbuPort}";
|
||||
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml down");
|
||||
await _context.Bash(
|
||||
// Set the log level to ERROR to prevent warnings concerning environment variables not being set.
|
||||
$"docker --log-level ERROR compose -f {Features.Db.FeatureName}.docker-compose.yml down"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,12 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresFeature(Features.Db.FeatureName)]
|
||||
public partial class Db : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
public Db(IEnumerable<ISubCommandOf<Db>> commands)
|
||||
|
@ -1,7 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using Humanizer;
|
||||
using MycroForge.Core.Contract;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
@ -60,10 +58,10 @@ public partial class MycroForge
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var folderPath = string.Empty;
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
||||
if (fqn.HasNamespace)
|
||||
folderPath = Path.Join(folderPath, fqn.Namespace);
|
||||
|
||||
var filePath = Path.Combine(folderPath, $"{fqn.SnakeCasedName}.py");
|
||||
var filePath = Path.Join(folderPath, $"{fqn.SnakeCasedName}.py");
|
||||
var template = withSession ? WithSessionTemplate : DefaultTemplate;
|
||||
var code = string.Join('\n', template)
|
||||
.Replace("%class_name%", fqn.PascalizedName);
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@ -6,11 +7,12 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresFile("requirements.txt")]
|
||||
public class Hydrate : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Hydrate(ProjectContext context)
|
||||
|
||||
public Hydrate(ProjectContext context)
|
||||
: base("hydrate", "Initialize venv and install dependencies from requirements.txt")
|
||||
{
|
||||
_context = context;
|
||||
|
@ -15,6 +15,7 @@ public partial class MycroForge
|
||||
ApiPort = ctx.ParseResult.GetValueForOption(ApiPortOption),
|
||||
DbhPort = ctx.ParseResult.GetValueForOption(DbhPortOption),
|
||||
DbuPort = ctx.ParseResult.GetValueForOption(DbuPortOption),
|
||||
DbuPlatform = ctx.ParseResult.GetValueForOption(DbuPlatformOption),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
namespace MycroForge.CLI.Commands;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
@ -11,6 +13,7 @@ public partial class MycroForge
|
||||
public int? ApiPort { get; set; }
|
||||
public int? DbhPort { get; set; }
|
||||
public int? DbuPort { get; set; }
|
||||
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
|
||||
|
||||
public Features.Api.Options ApiOptions => new()
|
||||
{
|
||||
@ -20,7 +23,8 @@ public partial class MycroForge
|
||||
public Features.Db.Options DbOptions => new()
|
||||
{
|
||||
DbhPort = DbhPort <= 0 ? 5050 : DbhPort,
|
||||
DbuPort = DbuPort <= 0 ? 5051 : DbhPort
|
||||
DbuPort = DbuPort <= 0 ? 5051 : DbhPort,
|
||||
DbuPlatform = DbuPlatform
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -39,6 +39,11 @@ public partial class MycroForge
|
||||
description: "The database UI port"
|
||||
);
|
||||
|
||||
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
|
||||
aliases: ["--database-ui-platform", "--dbu-platform"],
|
||||
description: "The docker platform for the PhpMyAdmin image"
|
||||
);
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
private readonly List<IFeature> _features;
|
||||
private readonly OptionsContainer _optionsContainer;
|
||||
@ -55,6 +60,7 @@ public partial class MycroForge
|
||||
AddOption(ApiPortOption);
|
||||
AddOption(DbhPortOption);
|
||||
AddOption(DbuPortOption);
|
||||
AddOption(DbuPlatformOption);
|
||||
|
||||
this.SetHandler(ExecuteAsync, new Binder());
|
||||
}
|
||||
@ -75,7 +81,7 @@ public partial class MycroForge
|
||||
await _context.CreateFile("main.py");
|
||||
|
||||
// Create the venv
|
||||
await _context.Bash($"python3 -m venv {Path.Combine(projectRoot, ".venv")}");
|
||||
await _context.Bash($"python3 -m venv {Path.Join(projectRoot, ".venv")}");
|
||||
|
||||
// Pass feature arguments to the ArgsContainer
|
||||
_optionsContainer.Set(options.ApiOptions);
|
||||
@ -100,7 +106,7 @@ public partial class MycroForge
|
||||
|
||||
private async Task<string> CreateDirectory(string name)
|
||||
{
|
||||
var directory = Path.Combine(Directory.GetCurrentDirectory(), name);
|
||||
var directory = Path.Join(Directory.GetCurrentDirectory(), name);
|
||||
|
||||
if (Directory.Exists(directory))
|
||||
throw new Exception($"Directory {directory} already exists.");
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresVenv]
|
||||
public class Install : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
@ -28,7 +30,7 @@ public partial class MycroForge
|
||||
|
||||
if (packs.Length == 0)
|
||||
{
|
||||
Console.WriteLine("m4g install requires at least one package.");
|
||||
Console.WriteLine("'m4g install' requires at least one package.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -10,21 +10,29 @@ public partial class MycroForge
|
||||
{
|
||||
public class Init : Command, ISubCommandOf<Plugin>
|
||||
{
|
||||
private static readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of your project");
|
||||
private static readonly Argument<string> NamespaceArgument =
|
||||
new(name: "namespace", description: "The namespace of your project");
|
||||
|
||||
private static readonly Argument<string> ClassArgument =
|
||||
new(name: "class", description: "The class name of the generated command");
|
||||
|
||||
private static readonly Argument<string> CommandArgument =
|
||||
new(name: "command", description: "The command name that will be added to 'm4g'");
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Init(ProjectContext context) : base("init", "Initialize a basic plugin project")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(NameArgument);
|
||||
this.SetHandler(ExecuteAsync, NameArgument);
|
||||
AddArgument(NamespaceArgument);
|
||||
AddArgument(ClassArgument);
|
||||
AddArgument(CommandArgument);
|
||||
this.SetHandler(ExecuteAsync, NamespaceArgument, ClassArgument, CommandArgument);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(string name)
|
||||
private async Task ExecuteAsync(string @namespace, string @class, string command)
|
||||
{
|
||||
await _context.Bash($"dotnet new m4gp -n {name}");
|
||||
await _context.Bash($"dotnet new m4gp -n {@namespace} --class {@class} --command {command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Humanizer;
|
||||
using Microsoft.Extensions.FileSystemGlobbing;
|
||||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@ -11,6 +12,7 @@ public partial class MycroForge
|
||||
{
|
||||
public partial class Plugin
|
||||
{
|
||||
[RequiresPlugin]
|
||||
public class Install : Command, ISubCommandOf<Plugin>
|
||||
{
|
||||
public enum TargetPlatform
|
||||
@ -42,9 +44,13 @@ public partial class MycroForge
|
||||
var assemblyName = GetAssemblyName();
|
||||
var pluginInstallPath = Path.Join(Plugins.RootDirectory, assemblyName);
|
||||
var platform = target.ToString().Dasherize();
|
||||
await _context.Bash($"dotnet publish -c Release -r {platform} --output {pluginInstallPath}");
|
||||
Console.WriteLine($"Successfully installed plugin {assemblyName}");
|
||||
var exitCode = await _context.Bash(
|
||||
$"dotnet publish -c Release -r {platform} --output {pluginInstallPath}"
|
||||
);
|
||||
|
||||
Console.WriteLine(exitCode == 0
|
||||
? $"Successfully installed plugin {assemblyName}"
|
||||
: $"Could not install {assemblyName}, process exited with code {exitCode}.");
|
||||
}
|
||||
|
||||
private string GetAssemblyName()
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresVenv]
|
||||
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
@ -27,9 +29,17 @@ public partial class MycroForge
|
||||
|
||||
private async Task ExecuteAsync(IEnumerable<string> packages, bool yes)
|
||||
{
|
||||
var packs = packages.ToArray();
|
||||
|
||||
if (packs.Length == 0)
|
||||
{
|
||||
Console.WriteLine("'m4g uninstall' requires at least one package.");
|
||||
return;
|
||||
}
|
||||
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"pip uninstall{(yes ? " --yes " : " ")}{string.Join(' ', packages)}",
|
||||
$"pip uninstall{(yes ? " --yes " : " ")}{string.Join(' ', packs)}",
|
||||
"pip freeze > requirements.txt"
|
||||
);
|
||||
}
|
||||
|
96
MycroForge.CLI/Extensions/CommandExtensions.cs
Normal file
96
MycroForge.CLI/Extensions/CommandExtensions.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using System.CommandLine;
|
||||
using System.CommandLine.Builder;
|
||||
using System.CommandLine.Invocation;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Reflection;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
|
||||
namespace MycroForge.CLI.Extensions;
|
||||
|
||||
public static class CommandExtensions
|
||||
{
|
||||
public static async Task ExecuteAsync(this Commands.MycroForge rootCommand, string[] args)
|
||||
{
|
||||
var parser = new CommandLineBuilder(rootCommand)
|
||||
.AddMiddleware()
|
||||
.UseDefaults()
|
||||
.UseExceptionHandler((ex, ctx) =>
|
||||
{
|
||||
/*
|
||||
* Use a custom ExceptionHandler to prevent the System.CommandLine library from printing the StackTrace by default.
|
||||
*/
|
||||
Console.WriteLine(ex.Message);
|
||||
|
||||
/*
|
||||
* Set the exit code to a non-zero value to indicate to the shell that the process has failed.
|
||||
*/
|
||||
Environment.ExitCode = -1;
|
||||
})
|
||||
.Build();
|
||||
|
||||
await parser.InvokeAsync(args);
|
||||
}
|
||||
|
||||
private static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder)
|
||||
{
|
||||
builder.AddMiddleware(async (context, next) =>
|
||||
{
|
||||
var commandChain = context.GetCommandChain();
|
||||
var commandText = string.Join(' ', commandChain.Select(cmd => cmd.Name));
|
||||
|
||||
foreach (var _command in commandChain)
|
||||
{
|
||||
if (_command.GetRequiresFeatureAttribute() is RequiresFeatureAttribute requiresFeatureAttribute)
|
||||
requiresFeatureAttribute.RequireFeature(commandText);
|
||||
|
||||
else if (_command.GetRequiresFileAttribute() is RequiresFileAttribute requiresFileAttribute)
|
||||
requiresFileAttribute.RequireFile(commandText);
|
||||
|
||||
else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute)
|
||||
requiresPluginAttribute.RequirePluginProject(commandText);
|
||||
|
||||
else if (_command.GetRequiresVenvAttribute() is RequiresVenvAttribute requiresVenvAttribute)
|
||||
requiresVenvAttribute.RequireVenv(commandText);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
});
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static List<Command> GetCommandChain(this InvocationContext context)
|
||||
{
|
||||
var chain = new List<Command>();
|
||||
/*
|
||||
* The CommandResult property refers to the last command in the chain.
|
||||
* So if the command is 'm4g api run' the CommandResult will refer to 'run'.
|
||||
*/
|
||||
SymbolResult? cmd = context.ParseResult.CommandResult;
|
||||
|
||||
while (cmd is CommandResult result)
|
||||
{
|
||||
chain.Add(result.Command);
|
||||
cmd = cmd.Parent;
|
||||
}
|
||||
|
||||
/*
|
||||
* Reverse the chain to reflect the actual order of the commands.
|
||||
*/
|
||||
chain.Reverse();
|
||||
|
||||
return chain;
|
||||
}
|
||||
|
||||
private static RequiresFeatureAttribute? GetRequiresFeatureAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresFeatureAttribute>();
|
||||
|
||||
private static RequiresFileAttribute? GetRequiresFileAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresFileAttribute>();
|
||||
|
||||
private static RequiresPluginAttribute? GetRequiresPluginAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresPluginAttribute>();
|
||||
|
||||
private static RequiresVenvAttribute? GetRequiresVenvAttribute(this Command command) =>
|
||||
command.GetType().GetCustomAttribute<RequiresVenvAttribute>();
|
||||
}
|
@ -8,7 +8,7 @@ namespace MycroForge.CLI.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection RegisterCommandDefaults(this IServiceCollection services)
|
||||
public static IServiceCollection RegisterDefaultCommands(this IServiceCollection services)
|
||||
{
|
||||
// Register ProjectContext, OptionsContainer & features
|
||||
services.AddScoped<ProjectContext>();
|
||||
|
@ -2,11 +2,8 @@
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
public static string DeduplicateDots(this string path)
|
||||
{
|
||||
while (path.Contains(".."))
|
||||
path = path.Replace("..", ".");
|
||||
|
||||
return path.Trim('.');
|
||||
}
|
||||
public static string SlashesToDots(this string path) =>
|
||||
path.Replace('/', '.')
|
||||
.Replace('\\', '.')
|
||||
.Trim();
|
||||
}
|
@ -52,7 +52,7 @@ public sealed partial class Api : IFeature
|
||||
|
||||
await context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
"python3 -m pip install fastapi",
|
||||
"python3 -m pip install fastapi uvicorn",
|
||||
"python3 -m pip freeze > requirements.txt"
|
||||
);
|
||||
|
||||
|
@ -1,10 +1,15 @@
|
||||
namespace MycroForge.CLI.Features;
|
||||
using MycroForge.Core;
|
||||
|
||||
namespace MycroForge.CLI.Features;
|
||||
|
||||
public sealed partial class Db
|
||||
{
|
||||
public class Options
|
||||
{
|
||||
public int? DbhPort { get; set; }
|
||||
|
||||
public int? DbuPort { get; set; }
|
||||
|
||||
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
|
||||
}
|
||||
}
|
@ -37,7 +37,6 @@ public sealed partial class Db : IFeature
|
||||
|
||||
private static readonly string[] DockerCompose =
|
||||
[
|
||||
"version: '3.8'",
|
||||
"# Access the database UI at http://localhost:${DBU_PORT}.",
|
||||
"# Login: username = root & password = password",
|
||||
"",
|
||||
@ -58,7 +57,7 @@ public sealed partial class Db : IFeature
|
||||
" - '%app_name%_mariadb:/var/lib/mysql'",
|
||||
"",
|
||||
" %app_name%_phpmyadmin:",
|
||||
" image: phpmyadmin/phpmyadmin",
|
||||
" image: %dbu_platform%/phpmyadmin",
|
||||
" container_name: %app_name%_phpmyadmin",
|
||||
" ports:",
|
||||
" - '${DBU_PORT}:80'",
|
||||
@ -87,15 +86,16 @@ public sealed partial class Db : IFeature
|
||||
{
|
||||
_optionsContainer = optionsContainer;
|
||||
}
|
||||
|
||||
|
||||
public async Task ExecuteAsync(ProjectContext context)
|
||||
{
|
||||
var options = _optionsContainer.Get<Options>();
|
||||
var config = await context.LoadConfig(create: true);
|
||||
config.Db = new()
|
||||
{
|
||||
DbhPort = options.DbhPort ?? 5050,
|
||||
DbuPort = options.DbuPort ?? 5051
|
||||
DbhPort = options.DbhPort ?? 5050,
|
||||
DbuPort = options.DbuPort ?? 5051,
|
||||
DbuPlatform = options.DbuPlatform
|
||||
};
|
||||
await context.SaveConfig(config);
|
||||
|
||||
@ -123,7 +123,10 @@ public sealed partial class Db : IFeature
|
||||
|
||||
await context.CreateFile($"{FeatureName}/entities/entity_base.py", EntityBase);
|
||||
|
||||
var dockerCompose = string.Join('\n', DockerCompose).Replace("%app_name%", appName);
|
||||
var dockerCompose = string.Join('\n', DockerCompose)
|
||||
.Replace("%app_name%", appName)
|
||||
.Replace("%dbu_platform%", options.DbuPlatform.ToString())
|
||||
;
|
||||
|
||||
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>0.0.1</Version>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -1,25 +1,18 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using MycroForge.CLI.Extensions;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using RootCommand = MycroForge.CLI.Commands.MycroForge;
|
||||
|
||||
using var host = Host
|
||||
.CreateDefaultBuilder()
|
||||
.ConfigureServices((_, services) =>
|
||||
{
|
||||
services
|
||||
.RegisterCommandDefaults()
|
||||
.RegisterDefaultCommands()
|
||||
.RegisterCommandPlugins()
|
||||
;
|
||||
})
|
||||
.Build();
|
||||
|
||||
try
|
||||
{
|
||||
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>()
|
||||
.InvokeAsync(args.Length == 0 ? ["--help"] : args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e.Message);
|
||||
}
|
||||
var command = host.Services.GetRequiredService<RootCommand>();
|
||||
await command.ExecuteAsync(args.Length == 0 ? ["--help"] : args);
|
||||
|
@ -1,11 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
TARGET=$1
|
||||
|
||||
if [ -z "$TARGET" ]; then
|
||||
echo "The target platform was not provided."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dotnet publish --self-contained -r "$TARGET"
|
||||
zip -vr "releases/m4g-$TARGET.zip" "bin/Release/net8.0/$TARGET/"
|
@ -1,7 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
./scripts/build-executable.sh linux-x64
|
||||
./scripts/build-executable.sh linux-arm
|
||||
./scripts/build-executable.sh linux-arm64
|
||||
./scripts/build-executable.sh osx-x64
|
||||
./scripts/build-executable.sh osx-arm64
|
@ -1,21 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
ZIP=$1
|
||||
if [ -z "$ZIP" ]; then
|
||||
echo "The zip file was not provided."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET=${ZIP//"m4g-"/}
|
||||
TARGET=${TARGET//".zip"/}
|
||||
|
||||
DIR="/tmp/m4g"
|
||||
|
||||
rm -rf "$DIR"
|
||||
unzip "$ZIP" -d "$DIR"
|
||||
|
||||
sudo rm -rf /usr/share/m4g
|
||||
sudo cp -r "$DIR/bin/Release/net8.0/$TARGET" /usr/share/m4g
|
||||
|
||||
sudo unlink /usr/local/bin/m4g 2> /dev/null
|
||||
sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/local/bin/m4g
|
@ -1,19 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
ZIP=$1
|
||||
if [ -z "$ZIP" ]; then
|
||||
echo "The zip file was not provided."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET=${ZIP//"m4g-"/}
|
||||
TARGET=${TARGET//".zip"/}
|
||||
|
||||
DIR="/tmp/m4g"
|
||||
|
||||
rm -rf "$DIR"
|
||||
unzip "$ZIP" -d "$DIR"
|
||||
|
||||
sudo rm -rf /usr/local/bin/m4g
|
||||
sudo cp -r "$DIR/bin/Release/net8.0/$TARGET" /usr/local/bin/m4g
|
||||
mv /usr/share/m4g/MycroForge.CLI /usr/share/m4g/m4g
|
@ -1,11 +0,0 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
# Make sure to run this script from the MycroForge.CLI directory and prefixed with sudo!
|
||||
# Example:
|
||||
# sudo ./scripts/publish-linux.sh
|
||||
|
||||
dotnet publish --self-contained -r linux-x64
|
||||
sudo rm -rf /usr/share/m4g
|
||||
sudo cp -r bin/Release/net8.0/linux-x64 /usr/share/m4g
|
||||
sudo unlink /usr/local/bin/m4g
|
||||
sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/local/bin/m4g
|
@ -1,5 +1,7 @@
|
||||
#!/usr/bin/bash
|
||||
#!/usr/bin/bash
|
||||
|
||||
dotnet pack -v d
|
||||
|
||||
dotnet nuget push nupkg/MycroForge.CLI.1.0.0.nupkg --source devdisciples
|
||||
VERSION="1.0.0"
|
||||
|
||||
dotnet nuget push nupkg/MycroForge.CLI.$VERSION.nupkg --source devdisciples
|
||||
|
2
MycroForge.CLI/scripts/publish-dotnet.sh → MycroForge.CLI/scripts/publish-tool.sh
Normal file → Executable file
2
MycroForge.CLI/scripts/publish-dotnet.sh → MycroForge.CLI/scripts/publish-tool.sh
Normal file → Executable file
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
dotnet pack -v d
|
||||
|
@ -48,7 +48,7 @@ public class Source
|
||||
|
||||
public Source InsertMultiLineAtEnd(params string[] text)
|
||||
{
|
||||
_text += (string.Join('\n', text));
|
||||
_text += string.Join('\n', text);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
17
MycroForge.Core/DefaultJsonSerializerOptions.cs
Normal file
17
MycroForge.Core/DefaultJsonSerializerOptions.cs
Normal file
@ -0,0 +1,17 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public static class DefaultJsonSerializerOptions
|
||||
{
|
||||
public static readonly JsonSerializerOptions Default = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public static readonly JsonSerializerOptions CamelCasePrettyPrint = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
@ -4,15 +4,12 @@ namespace MycroForge.Core.Extensions;
|
||||
|
||||
public static class ObjectStreamExtensions
|
||||
{
|
||||
public static async Task<string> SerializeAsync(
|
||||
this object @object,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null
|
||||
)
|
||||
public static async Task<string> SerializeAsync(this object @object, JsonSerializerOptions? options = null)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var reader = new StreamReader(stream);
|
||||
var options = jsonSerializerOptions ?? Serialization.DefaultJsonSerializerOptions.Default;
|
||||
|
||||
options ??= DefaultJsonSerializerOptions.Default;
|
||||
|
||||
await JsonSerializer.SerializeAsync(stream, @object, options);
|
||||
stream.Position = 0;
|
||||
return await reader.ReadToEndAsync();
|
||||
|
@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>MycroForge.Core</PackageId>
|
||||
<Description>The MycroForge core package</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>0.0.1</Version>
|
||||
<Authors>Donné Napo</Authors>
|
||||
<Company>Dev Disciples</Company>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
|
16
MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs
Normal file
16
MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public partial class ProjectConfig
|
||||
{
|
||||
public partial class DbConfig
|
||||
{
|
||||
public enum DbuPlatformOptions
|
||||
{
|
||||
amd64,
|
||||
arm32v5,
|
||||
arm32v6,
|
||||
arm32v7,
|
||||
arm64v8
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +1,16 @@
|
||||
namespace MycroForge.Core;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public partial class ProjectConfig
|
||||
{
|
||||
public class DbConfig
|
||||
public partial class DbConfig
|
||||
{
|
||||
public int DbhPort { get; set; }
|
||||
|
||||
public int DbuPort { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public DbuPlatformOptions DbuPlatform { get; set; }
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ public class ProjectContext
|
||||
{
|
||||
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
||||
public string AppName => Path.GetFileNameWithoutExtension(RootDirectory).Underscore().ToLower();
|
||||
private string ConfigPath => Path.Combine(RootDirectory, "m4g.json");
|
||||
private string ConfigPath => Path.Join(RootDirectory, "m4g.json");
|
||||
|
||||
|
||||
public async Task<ProjectConfig> LoadConfig(bool create = false)
|
||||
@ -21,8 +21,8 @@ public class ProjectContext
|
||||
}
|
||||
|
||||
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
|
||||
File.OpenRead(ConfigPath),
|
||||
Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
||||
File.OpenRead(ConfigPath),
|
||||
DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
||||
);
|
||||
|
||||
if (config is null)
|
||||
@ -37,21 +37,9 @@ public class ProjectContext
|
||||
RootDirectory = path;
|
||||
}
|
||||
|
||||
public void AssertDirectoryExists(string path)
|
||||
{
|
||||
var fullPath = Path.Combine(RootDirectory, path);
|
||||
|
||||
if (!Directory.Exists(fullPath))
|
||||
{
|
||||
throw new(string.Join('\n',
|
||||
$"{fullPath} does not exist, make sure you're in the correct directory."
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateFile(string path, params string[] content)
|
||||
{
|
||||
var fullPath = Path.Combine(RootDirectory, path);
|
||||
var fullPath = Path.Join(RootDirectory, path);
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
|
||||
if (fileInfo.Exists) return;
|
||||
@ -59,11 +47,12 @@ public class ProjectContext
|
||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
await Bash($"chmod 777 {fullPath}");
|
||||
Console.WriteLine($"Created file {path}");
|
||||
}
|
||||
|
||||
public async Task<string> ReadFile(string path)
|
||||
{
|
||||
var fullPath = Path.Combine(RootDirectory, path);
|
||||
var fullPath = Path.Join(RootDirectory, path);
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
@ -74,13 +63,14 @@ public class ProjectContext
|
||||
|
||||
public async Task WriteFile(string path, params string[] content)
|
||||
{
|
||||
var fullPath = Path.Combine(RootDirectory, path);
|
||||
var fullPath = Path.Join(RootDirectory, path);
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
Console.WriteLine($"Modified file {path}");
|
||||
}
|
||||
|
||||
public async Task Bash(params string[] script)
|
||||
public async Task<int> Bash(params string[] script)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
@ -114,21 +104,21 @@ public class ProjectContext
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
await using var input = process.StandardInput;
|
||||
foreach (var line in script)
|
||||
await input.WriteLineAsync(line);
|
||||
// Concat with '&&' operator to make sure that script does not continue on failure.
|
||||
await input.WriteAsync(string.Join(" && ", script));
|
||||
|
||||
await input.FlushAsync();
|
||||
input.Close();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
Console.WriteLine($"Process finished with exit code {process.ExitCode}.");
|
||||
Environment.ExitCode = process.ExitCode;
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
|
||||
public async Task SaveConfig(ProjectConfig config)
|
||||
{
|
||||
var json = await config.SerializeAsync(Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint);
|
||||
var json = await config.SerializeAsync(DefaultJsonSerializerOptions.CamelCasePrettyPrint);
|
||||
await File.WriteAllTextAsync(ConfigPath, json);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace MycroForge.Core;
|
||||
|
||||
public static class Serialization
|
||||
{
|
||||
public static class DefaultJsonSerializerOptions
|
||||
{
|
||||
public static readonly JsonSerializerOptions Default = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
public static readonly JsonSerializerOptions CamelCasePrettyPrint = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
}
|
||||
}
|
5
MycroForge.Core/scripts/publish-nuget.sh
Executable file
5
MycroForge.Core/scripts/publish-nuget.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
VERSION=$(grep '<Version>' < MycroForge.Core.csproj | sed 's/.*<Version>\(.*\)<\/Version>/\1/' | tr -d '[:space:]')
|
||||
dotnet build -r Releasedo
|
||||
dotnet nuget push --source devdisciples "bin/Release/MycroForge.Core.$VERSION.nupkg"
|
@ -4,7 +4,7 @@
|
||||
<!-- The package metadata. Fill in the properties marked as TODO below -->
|
||||
<!-- Follow the instructions on https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices -->
|
||||
<PackageId>MycroForge.PluginTemplate.Package</PackageId>
|
||||
<PackageVersion>1.0</PackageVersion>
|
||||
<PackageVersion>0.0.1</PackageVersion>
|
||||
<Title>A template for generating MycroForge plugins</Title>
|
||||
<Authors>Donné Napo</Authors>
|
||||
<Description>Template to use when creating a plugin for the MycroForge CLI.</Description>
|
||||
|
@ -8,7 +8,7 @@ https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-tem
|
||||
### Build the package
|
||||
`dotnet pack`
|
||||
|
||||
### Push to local nuget
|
||||
### Push to devdisciples nuget
|
||||
`dotnet nuget push bin/Release/MycroForge.PluginTemplate.Package.1.0.0.nupkg --source devdisciples`
|
||||
|
||||
### Install template package from local nuget
|
2
MycroForge.PluginTemplate.Package/scripts/build_package.sh → MycroForge.PluginTemplate.Package/scripts/build-package.sh
Normal file → Executable file
2
MycroForge.PluginTemplate.Package/scripts/build_package.sh → MycroForge.PluginTemplate.Package/scripts/build-package.sh
Normal file → Executable file
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/env bash
|
||||
|
||||
rm -rf templates/MycroForge.PluginTemplate
|
||||
cp -R ../MycroForge.PluginTemplate templates/MycroForge.PluginTemplate
|
12
MycroForge.PluginTemplate.Package/scripts/publish-nuget.sh
Executable file
12
MycroForge.PluginTemplate.Package/scripts/publish-nuget.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/env bash
|
||||
|
||||
rm -rf templates/MycroForge.PluginTemplate
|
||||
cp -R ../MycroForge.PluginTemplate templates/MycroForge.PluginTemplate
|
||||
dotnet pack
|
||||
|
||||
# Set the path to the package
|
||||
VERSION=$(grep '<PackageVersion>' < MycroForge.PluginTemplate.Package.csproj | sed 's/.*<PackageVersion>\(.*\)<\/PackageVersion>/\1/' | tr -d '[:space:]')
|
||||
PACKAGE="bin/Release/MycroForge.PluginTemplate.Package.$VERSION.nupkg"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
@ -17,5 +17,16 @@
|
||||
"language": "C#",
|
||||
"type": "project"
|
||||
},
|
||||
"preferNameDirectory": true
|
||||
"preferNameDirectory": true,
|
||||
"symbols": {
|
||||
"class": {
|
||||
"type": "parameter",
|
||||
"replaces": "ExampleCommand",
|
||||
"fileRename": "ExampleCommand"
|
||||
},
|
||||
"command": {
|
||||
"type": "parameter",
|
||||
"replaces": "example"
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ using RootCommand = MycroForge.Core.RootCommand;
|
||||
|
||||
namespace MycroForge.PluginTemplate;
|
||||
|
||||
public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
|
||||
public class ExampleCommand : Command, ISubCommandOf<RootCommand>
|
||||
{
|
||||
private readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of the person to greet");
|
||||
@ -15,8 +15,8 @@ public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public HelloWorldCommand(ProjectContext context) :
|
||||
base("hello", "An example command generated by dotnet new using the m4gp template")
|
||||
public ExampleCommand(ProjectContext context) :
|
||||
base("example", "A basic command plugin generated by the 'm4g plugin init' command")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(NameArgument);
|
||||
@ -28,9 +28,9 @@ public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
|
||||
{
|
||||
name = allCaps ? name.ToUpper() : name;
|
||||
|
||||
await _context.CreateFile("hello_world.txt",
|
||||
await _context.CreateFile("example.txt",
|
||||
$"Hello {name}!",
|
||||
"This file was generated by your custom command!"
|
||||
"This file was generated by the 'm4g example' command!"
|
||||
);
|
||||
}
|
||||
}
|
@ -4,12 +4,12 @@ using RootCommand = MycroForge.Core.RootCommand;
|
||||
|
||||
namespace MycroForge.PluginTemplate;
|
||||
|
||||
public class HelloWorldCommandPlugin : ICommandPlugin
|
||||
public class ExampleCommandPlugin : ICommandPlugin
|
||||
{
|
||||
public string Name => "MycroForge.PluginTemplate";
|
||||
|
||||
public void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
|
||||
services.AddScoped<ISubCommandOf<RootCommand>, ExampleCommand>();
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MycroForge.Core" Version="1.0.0" />
|
||||
<PackageReference Include="MycroForge.Core" Version="0.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate.Package", "MycroForge.PluginTemplate.Package\MycroForge.PluginTemplate.Package.csproj", "{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI.Tests", "MycroForge.CLI.Tests\MycroForge.CLI.Tests.csproj", "{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@ -30,5 +32,9 @@ Global
|
||||
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
30
README.md
30
README.md
@ -1,29 +1,3 @@
|
||||
### Dependencies
|
||||
### Documentation
|
||||
|
||||
- git
|
||||
- Docker
|
||||
- bash (/bin/bash)
|
||||
- Python 3.10.2 (/usr/bin/python3)
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
|
||||
#### Note
|
||||
The MycroForge CLI assumes a linux compatible environment, so on Windows you'll have to use WSL.
|
||||
`Ubuntu-22.04` is the recommended WSL version to use.
|
||||
|
||||
### TODO
|
||||
- Figure out why BashException cannot be caught, can it be due to the differences in scoping?
|
||||
Because the `Bash` class is static and the services calling `Bash.ExecuteAsync` are in the container.
|
||||
Maybe this in combination with the async nature of the whole thing?
|
||||
|
||||
### Install
|
||||
|
||||
Run the install script in the same directory as the downloaded zip. See the example below for linux-x64.
|
||||
|
||||
`sudo ./install.sh m4g-<platform>.zip <platform>`
|
||||
|
||||
### Add DevDisciples NuGet source
|
||||
|
||||
```bash
|
||||
dotnet nuget add source --name devdisciples --username username --password password https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json --store-password-in-clear-text
|
||||
```
|
||||
Go to https://m4g.devdisciples.com for the docs.
|
20
docs/.gitignore
vendored
20
docs/.gitignore
vendored
@ -1,20 +0,0 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
@ -1,41 +0,0 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ GIT_USER=<Your GitHub username> yarn deploy
|
||||
```
|
||||
|
||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
@ -1,24 +0,0 @@
|
||||
# Commands
|
||||
|
||||
```
|
||||
Description:
|
||||
The MycroForge CLI tool.
|
||||
|
||||
Usage:
|
||||
m4g [command] [options]
|
||||
|
||||
Options:
|
||||
--version Show version information
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
init <name> Initialize a new project
|
||||
i, install <packages> Install packages and update the requirements.txt
|
||||
u, uninstall <packages> Uninstall packages and update the requirements.txt
|
||||
hydrate Initialize venv and install dependencies from requirements.txt
|
||||
add Add features to the project
|
||||
g, generate Generate common items
|
||||
api API related commands
|
||||
db Database related commands
|
||||
p, plugin Plugin related commands
|
||||
```
|
@ -1,22 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add
|
||||
|
||||
```
|
||||
Description:
|
||||
Add features to the project
|
||||
|
||||
Usage:
|
||||
m4g add [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
api Add FastAPI to the project
|
||||
db Add SQLAlchemy & Alembic to the project
|
||||
git Add git to the project
|
||||
gitignore Add a default .gitignore file to the project
|
||||
```
|
@ -1,17 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add api
|
||||
|
||||
```
|
||||
Description:
|
||||
Add FastAPI to the project
|
||||
|
||||
Usage:
|
||||
m4g add api [options]
|
||||
|
||||
Options:
|
||||
--api-port <api-port> The API port
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,18 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add db
|
||||
|
||||
```
|
||||
Description:
|
||||
Add SQLAlchemy & Alembic to the project
|
||||
|
||||
Usage:
|
||||
m4g add db [options]
|
||||
|
||||
Options:
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add git
|
||||
|
||||
```
|
||||
Description:
|
||||
Add git to the project
|
||||
|
||||
Usage:
|
||||
m4g add git [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add gitignore
|
||||
|
||||
```
|
||||
Description:
|
||||
Add a default .gitignore file to the project
|
||||
|
||||
Usage:
|
||||
m4g add gitignore [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api
|
||||
|
||||
```
|
||||
Description:
|
||||
API related commands
|
||||
|
||||
Usage:
|
||||
m4g api [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
run Run your app
|
||||
g, generate Generate an API item
|
||||
```
|
@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api generate
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate an API item
|
||||
|
||||
Usage:
|
||||
m4g api generate [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
r, router <name> Generate an api router
|
||||
crud <entity> Generated CRUD functionality for an entity
|
||||
```
|
@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api generate crud
|
||||
|
||||
```
|
||||
Description:
|
||||
Generated CRUD functionality for an entity
|
||||
|
||||
Usage:
|
||||
m4g api generate crud <entity> [options]
|
||||
|
||||
Arguments:
|
||||
<entity> The entity to target
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api generate router
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate an api router
|
||||
|
||||
Usage:
|
||||
m4g api generate router <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the api router
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api run
|
||||
|
||||
```
|
||||
Description:
|
||||
Run your app
|
||||
|
||||
Usage:
|
||||
m4g api run [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,24 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db
|
||||
|
||||
```
|
||||
Description:
|
||||
Database related commands
|
||||
|
||||
Usage:
|
||||
m4g db [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
run Runs the services defined in db.docker-compose.yml
|
||||
stop Stops db.docker-compose.yml
|
||||
migrate Apply migrations to the database
|
||||
rollback Rollback the last migration
|
||||
g, generate Generate a database item
|
||||
link Define relationships between entities
|
||||
```
|
@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db generate
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate a database item
|
||||
|
||||
Usage:
|
||||
m4g db generate [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
e, entity <name> Generate and database entity
|
||||
m, migration <name> Generate a migration
|
||||
```
|
@ -1,35 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db generate entity
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate and database entity
|
||||
|
||||
Usage:
|
||||
m4g db generate entity <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the database entity
|
||||
|
||||
Supported formats:
|
||||
Entity
|
||||
path/relative/to/entities:Entity
|
||||
|
||||
Options:
|
||||
-c, --column <column> Specify the fields to add.
|
||||
|
||||
Format:
|
||||
<name>:<native_type>:<orm_type>
|
||||
|
||||
<name> = Name of the column
|
||||
<native_type> = The native Python type
|
||||
<orm_type> = The SQLAlchemy type
|
||||
|
||||
Example:
|
||||
first_name:str:String(255)
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
```
|
@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db generate migration
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate a migration
|
||||
|
||||
Usage:
|
||||
m4g db generate migration <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the migration
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db link
|
||||
|
||||
```
|
||||
Description:
|
||||
Define relationships between entities
|
||||
|
||||
Usage:
|
||||
m4g db link [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
one <entity> Define a 1:n relation
|
||||
many <entity> Define a n:m relation
|
||||
|
||||
```
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db link many
|
||||
|
||||
```
|
||||
Description:
|
||||
Define a n:m relation
|
||||
|
||||
Usage:
|
||||
m4g db link many <entity> [options]
|
||||
|
||||
Arguments:
|
||||
<entity> The left side of the relation
|
||||
|
||||
Options:
|
||||
--to-one <to-one> The right side of the relation
|
||||
--to-many <to-many> The right side of the relation
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db link one
|
||||
|
||||
```
|
||||
Description:
|
||||
Define a 1:n relation
|
||||
|
||||
Usage:
|
||||
m4g db link one <entity> [options]
|
||||
|
||||
Arguments:
|
||||
<entity> The left side of the relation
|
||||
|
||||
Options:
|
||||
--to-one <to-one> The right side of the relation
|
||||
--to-many <to-many> The right side of the relation
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db migrate
|
||||
|
||||
```
|
||||
Description:
|
||||
Apply migrations to the database
|
||||
|
||||
Usage:
|
||||
m4g db migrate [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db rollback
|
||||
|
||||
```
|
||||
Description:
|
||||
Rollback the last migration
|
||||
|
||||
Usage:
|
||||
m4g db rollback [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db run
|
||||
|
||||
```
|
||||
Description:
|
||||
Runs the services defined in db.docker-compose.yml
|
||||
|
||||
Usage:
|
||||
m4g db run [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db stop
|
||||
|
||||
```
|
||||
Description:
|
||||
Stops db.docker-compose.yml
|
||||
|
||||
Usage:
|
||||
m4g db stop [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g generate
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate common items
|
||||
|
||||
Usage:
|
||||
m4g generate [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
s, service <name> Generate a service
|
||||
venv Generate a venv
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user