Ading comments and cleaning up

This commit is contained in:
mdnapo 2024-07-22 06:24:45 +02:00
parent 5698b504e9
commit aa1c2422ef
30 changed files with 312 additions and 148 deletions

View File

@ -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.FolderPath, $"{fqn.SnakeCasedName}_service"
);
var serviceImportPath = serviceFilePath.SlashesToDots();
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.FolderPath);
var routerFilePath = Path.Join(routerFolderPath, $"{fqn.SnakeCasedName}");
var routerImportPath = routerFolderPath.SlashesToDots();
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.FolderPath);
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);

View File

@ -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);

View File

@ -148,7 +148,7 @@ public partial class EntityLinker
var path = $"{Features.Db.FeatureName}/entities";
if (fqn.HasPath)
path = Path.Combine(path, fqn.Path);
path = Path.Combine(path, fqn.FolderPath);
path = Path.Combine(path, $"{fqn.SnakeCasedName}.py");
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));

View File

@ -38,12 +38,14 @@ public class MainModifier
public MainModifier IncludeRouter(string prefix, string router)
{
_routerIncludeBuffer.Add($"\napp.include_router(prefix=\"/{prefix}\", router={router}.router)");
_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);
}
}
}

View File

@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Humanizer;
using MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.CodeGen;
@ -40,29 +41,30 @@ 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.FolderPath,
// requestsFolderPath,
$"{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("%entity_class_name%", entityClassName)
.Replace("%fields%", fields)
;
await _context.CreateFile(updateRequestFilePath, service);
await _context.CreateFile(requestFilePath, service);
}
private string ToFieldString(Field field, Type type)
@ -104,7 +106,7 @@ public class RequestClassGenerator
.Replace("]", "")
.Replace(" ", "")
.Split();
foreach (var dissectedType in dissectedTypes)
{
if (imports.FirstOrDefault(i => i.Match(dissectedType)) is Import import)

View File

@ -1,28 +1,48 @@
using Humanizer;
using MycroForge.CLI.Extensions;
namespace MycroForge.CLI.Commands;
public class FullyQualifiedName
{
public string Path { get; }
public string FolderPath { get; }
public string PascalizedName { get; }
public string SnakeCasedName { get; }
public bool HasPath => Path.Length > 0;
public string FilePath =>
string.IsNullOrEmpty(FolderPath.Trim())
? SnakeCasedName
: Path.Join(FolderPath, SnakeCasedName);
public bool HasPath => FolderPath.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;
FolderPath = 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('/', '.');
}

View File

@ -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);
}

View File

@ -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);
}
}
}

View File

@ -49,7 +49,7 @@ public partial class MycroForge
_context.AssertDirectoryExists(folderPath);
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
folderPath = Path.Combine(folderPath, fqn.FolderPath);
var fileName = $"{fqn.SnakeCasedName}.py";
var filePath = Path.Combine(folderPath, fileName);

View File

@ -75,7 +75,7 @@ public partial class MycroForge
_context.AssertDirectoryExists(Features.Db.FeatureName);
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
folderPath = Path.Combine(folderPath, fqn.FolderPath);
var _columns = GetColumnDefinitions(columns.ToArray());
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());

View File

@ -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);
}
}
}

View File

@ -22,6 +22,7 @@ public partial class MycroForge
private async Task ExecuteAsync()
{
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"
);
}

View File

@ -61,7 +61,7 @@ public partial class MycroForge
var folderPath = string.Empty;
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
folderPath = Path.Combine(folderPath, fqn.FolderPath);
var filePath = Path.Combine(folderPath, $"{fqn.SnakeCasedName}.py");
var template = withSession ? WithSessionTemplate : DefaultTemplate;

View File

@ -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),
};
}
}

View File

@ -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
};
}
}

View File

@ -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());
}

View File

@ -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();
}

View File

@ -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; }
}
}

View File

@ -1,6 +1,7 @@
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands;
using MycroForge.Core;
using MycroForge.Core.Extensions;
namespace MycroForge.CLI.Features;
@ -37,7 +38,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",
"",
@ -59,6 +59,7 @@ public sealed partial class Db : IFeature
"",
" %app_name%_phpmyadmin:",
" image: phpmyadmin/phpmyadmin",
" platform: %dbu_platform%",
" container_name: %app_name%_phpmyadmin",
" ports:",
" - '${DBU_PORT}:80'",
@ -87,15 +88,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 +125,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.ToDockerPlatformString())
;
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
}

View File

@ -1,4 +1,4 @@
#!/usr/bin/bash
#!/usr/bin/bash
dotnet pack -v d

View File

@ -0,0 +1,6 @@
namespace MycroForge.Core.Attributes;
public class DockerPlatformAttribute : Attribute
{
public string Platform { get; set; } = string.Empty;
}

View File

@ -0,0 +1,19 @@
using System.Reflection;
using MycroForge.Core.Attributes;
namespace MycroForge.Core.Extensions;
public static class EnumExtensions
{
/// <summary>
/// A generic extension method that aids in reflecting
/// and retrieving any attribute that is applied to an `Enum`.
/// </summary>
public static string ToDockerPlatformString(this ProjectConfig.DbConfig.DbuPlatformOptions value)
{
return value.GetType()
.GetMember(value.ToString())
.FirstOrDefault()!
.GetCustomAttribute<DockerPlatformAttribute>()!.Platform;
}
}

View File

@ -0,0 +1,27 @@
using MycroForge.Core.Attributes;
namespace MycroForge.Core;
public partial class ProjectConfig
{
public partial class DbConfig
{
public enum DbuPlatformOptions
{
[DockerPlatform(Platform = "linux/amd64")]
linux_amd64,
[DockerPlatform(Platform = "linux/arm32/v5")]
linux_arm32v5,
[DockerPlatform(Platform = "linux/arm32/v6")]
linux_arm32v6,
[DockerPlatform(Platform = "linux/arm32/v7")]
linux_arm32v7,
[DockerPlatform(Platform = "linux/arm64/v8")]
linux_arm64v8
}
}
}

View File

@ -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; }
}
}

View File

@ -26,4 +26,13 @@ Run the install script in the same directory as the downloaded zip. See the exam
```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
```
```
### TODO
- Fix `-c` option for `m4g db generate entity`
- Mention `--dbu-platform` option for `m4g init`
- Research if System.CommandLine middleware can be used to safeguard commands like `m4g add` or `m4g api`.
- Fix up exception handling
- Clean up README files
-

View File

@ -12,7 +12,8 @@ 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
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
-?, -h, --help Show help and usage information
```

View File

@ -15,9 +15,10 @@ Arguments:
<name> The name of your project
Options:
--without <api|db|git|gitignore> Features to exclude
--api-port <api-port> The API port
--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
--without <api|db|git|gitignore> Features to exclude
--api-port <api-port> The API port
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
-?, -h, --help Show help and usage information
```

View File

@ -13,10 +13,7 @@ To use MycroForge, ensure you have the following dependencies installed:
- **Python 3.10**
- **Docker**
- **.NET 8**
### Windows
For Windows users, MycroForge is designed to run in a POSIX compliant environment. It has been tested on Windows using WSL2 with Ubuntu 22.04.03. Therefore, it is recommended to run MycroForge within the same or a similar WSL2 distribution for optimal performance.
- **XCode Command Line Tools (MacOS only)**
### Adding the Package Registry
@ -37,3 +34,35 @@ dotnet tool install -g MycroForge.CLI
```
dotnet tool install -g MycroForge.CLI
```
### Windows
MycroForge is designed to run in a POSIX compliant environment. It has been tested on Windows using WSL2 with Ubuntu 22.04.03. So it is recommended to run MycroForge within the same or a similar WSL2 distribution for optimal performance.
### MacOS
#### Post install steps
After installing MycroForge, the dotnet CLI will show a message with some instructions to make the `m4g` command available in `zsh`.
It should look similar to the example below.
```sh
Tools directory '/Users/username/.dotnet/tools' is not currently on the PATH environment variable.
If you are using zsh, you can add it to your profile by running the following command:
cat << \EOF >> ~/.zprofile
# Add .NET Core SDK tools
export PATH="$PATH:/Users/username/.dotnet/tools"
EOF
And run zsh -l to make it available for current session.
You can only add it to the current session by running the following command:
export PATH="$PATH:/Users/username/.dotnet/tools"
```
#### Known issues
##### FastAPI swagger blank screen
If you see a blank screen when opening the FastAPI Swagger documentation, then make sure you've activated the Safari developer tools.

View File

@ -5,7 +5,7 @@ sidebar_position: 1
# Intro
Welcome to **MycroForge** an opinionated CLI tool designed to streamline the development of FastAPI and SQLAlchemy-based backends. With MycroForge, you can effortlessly create and manage your backend projects through a powerful command line interface.
Welcome to **MycroForge** an opinionated CLI tool designed to streamline the development of FastAPI and SQLAlchemy-based backends. With MycroForge, you can effortlessly create backend projects through a convenient command line interface.
## Key Features
@ -14,9 +14,3 @@ Welcome to **MycroForge** an opinionated CLI tool designed to streamline the
- **Migrations:** Handle database migrations seamlessly, allowing for smooth transitions and updates.
- **Routers:** Generate and manage routers to keep your application modular and organized.
- **CRUD Functionality:** Automatically generate basic CRUD (Create, Read, Update, Delete) operations to accelerate your development process.
## Purpose
The primary goal of MycroForge is to reduce the boilerplate code and provide a unified interface for common development tasks. By leveraging this tool, developers can focus more on writing business logic rather than repetitive setup and configuration.
Get started with MycroForge and enhance your backend development efficiency today!

View File

@ -28,7 +28,9 @@ locally.
The first step is to start the database, you can do this by running the following command in a terminal.
`m4g db run`
```bash
m4g db run
```
This command starts the services defined in the `db.docker-compose.yml` file.
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
@ -47,9 +49,13 @@ When you're done developing, you can shut down the local database by running `m4
Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
`Tag` entities.
`m4g db generate entity Tag --column "description:str:String(255)"`
```bash
m4g db generate entity Tag --column "description:str:String(255)"
```
`m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"`
```bash
m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"
```
After running these commands, you should find the generated entities in the `db/entities` folder of your project.
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
@ -65,7 +71,9 @@ Creating a one-to-many relation would also make sense, but for the purpose of de
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
in the database schema.
`m4g db link many Todo --to-many Tag`
```bash
m4g db link many Todo --to-many Tag
```
After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the
a `List` containing instances of the other entity.
@ -79,7 +87,9 @@ examine the command. The same is true for all the other commands as well.
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
Generate the initial migration by running the following command.
`m4g db generate migration initial_migration`
```bash
m4g db generate migration initial_migration
```
After running this command, you should see the new migration in the `db/version` directory.
@ -88,7 +98,9 @@ After running this command, you should see the new migration in the `db/version`
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
the following command.
`m4g db migrate`
```bash
m4g db migrate
```
After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
@ -102,9 +114,13 @@ Writing this code can be boring, since it's pretty much boilerplate with some cu
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
generate CRUD functionality for the `Todo` & `Tag` classes.
`m4g api generate crud Tag`
```bash
m4g api generate crud Tag
```
`m4g api generate crud Todo`
```bash
m4g api generate crud Todo
```
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
@ -118,15 +134,22 @@ yet. We need to be able to specify which `Tags` to add to a `Todo` when creating
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
This field will contain the ids of the `Tags` that are associated with a `Todo`.
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing`.
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`.
```python
# Before
from pydantic import BaseModel
class CreateTodoRequest(BaseModel):
description: str = None
is_done: bool = None
description: str = None
is_done: bool = None
# After
from typing import List, Optional
from pydantic import BaseModel
class CreateTodoRequest(BaseModel):
description: str = None
is_done: bool = None
@ -137,12 +160,19 @@ Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might n
```python
# Before
from pydantic import BaseModel
from typing import Optional
class UpdateTodoRequest(BaseModel):
description: Optional[str] = None
is_done: Optional[bool] = None
tag_ids: Optional[List[int]] = []
description: Optional[str] = None
is_done: Optional[bool] = None
# After
from pydantic import BaseModel
from typing import List, Optional
class UpdateTodoRequest(BaseModel):
description: Optional[str] = None
is_done: Optional[bool] = None
@ -280,4 +310,9 @@ Modify `TodoService.update`
## Test the API!
Run the following command.
```bash
m4g api run
```
Go to http://localhost:5000/docs and test your Todo API!