diff --git a/MycroForge.CLI/CodeGen/CrudRouterGenerator.cs b/MycroForge.CLI/CodeGen/CrudRouterGenerator.cs index 8eb9cd3..975a431 100644 --- a/MycroForge.CLI/CodeGen/CrudRouterGenerator.cs +++ b/MycroForge.CLI/CodeGen/CrudRouterGenerator.cs @@ -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); diff --git a/MycroForge.CLI/CodeGen/CrudServiceGenerator.cs b/MycroForge.CLI/CodeGen/CrudServiceGenerator.cs index 8341790..cf65b5c 100644 --- a/MycroForge.CLI/CodeGen/CrudServiceGenerator.cs +++ b/MycroForge.CLI/CodeGen/CrudServiceGenerator.cs @@ -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); diff --git a/MycroForge.CLI/CodeGen/EntityLinker.cs b/MycroForge.CLI/CodeGen/EntityLinker.cs index 8221ad5..3b2f505 100644 --- a/MycroForge.CLI/CodeGen/EntityLinker.cs +++ b/MycroForge.CLI/CodeGen/EntityLinker.cs @@ -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)); diff --git a/MycroForge.CLI/CodeGen/MainModifier.cs b/MycroForge.CLI/CodeGen/MainModifier.cs index 20e8435..5e2488b 100644 --- a/MycroForge.CLI/CodeGen/MainModifier.cs +++ b/MycroForge.CLI/CodeGen/MainModifier.cs @@ -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); } } } \ No newline at end of file diff --git a/MycroForge.CLI/CodeGen/RequestClassGenerator.cs b/MycroForge.CLI/CodeGen/RequestClassGenerator.cs index 32df4fd..41e1bc7 100644 --- a/MycroForge.CLI/CodeGen/RequestClassGenerator.cs +++ b/MycroForge.CLI/CodeGen/RequestClassGenerator.cs @@ -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) diff --git a/MycroForge.CLI/Commands/FullyQualifiedName.cs b/MycroForge.CLI/Commands/FullyQualifiedName.cs index 09bcf96..528ea4b 100644 --- a/MycroForge.CLI/Commands/FullyQualifiedName.cs +++ b/MycroForge.CLI/Commands/FullyQualifiedName.cs @@ -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('/', '.'); +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Add.Db.cs b/MycroForge.CLI/Commands/MycroForge.Add.Db.cs index 60f99b3..29147e3 100644 --- a/MycroForge.CLI/Commands/MycroForge.Add.Db.cs +++ b/MycroForge.CLI/Commands/MycroForge.Add.Db.cs @@ -21,6 +21,11 @@ public partial class MycroForge description: "The database UI port" ); + private static readonly Option 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 _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); } diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Crud.cs b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Crud.cs index f57b1d0..eec99ac 100644 --- a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Crud.cs +++ b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Crud.cs @@ -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); } } } diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs index d19e4e8..b130909 100644 --- a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs +++ b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs @@ -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); diff --git a/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs b/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs index 27d40a4..3253815 100644 --- a/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs +++ b/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs @@ -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()); diff --git a/MycroForge.CLI/Commands/MycroForge.Db.Run.cs b/MycroForge.CLI/Commands/MycroForge.Db.Run.cs index f95b26f..377a679 100644 --- a/MycroForge.CLI/Commands/MycroForge.Db.Run.cs +++ b/MycroForge.CLI/Commands/MycroForge.Db.Run.cs @@ -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); } } } diff --git a/MycroForge.CLI/Commands/MycroForge.Db.Stop.cs b/MycroForge.CLI/Commands/MycroForge.Db.Stop.cs index 51a88f5..810d278 100644 --- a/MycroForge.CLI/Commands/MycroForge.Db.Stop.cs +++ b/MycroForge.CLI/Commands/MycroForge.Db.Stop.cs @@ -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" ); } diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs index a5e7bee..000eceb 100644 --- a/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs +++ b/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs @@ -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; diff --git a/MycroForge.CLI/Commands/MycroForge.Init.Binder.cs b/MycroForge.CLI/Commands/MycroForge.Init.Binder.cs index 7cd369c..99c6294 100644 --- a/MycroForge.CLI/Commands/MycroForge.Init.Binder.cs +++ b/MycroForge.CLI/Commands/MycroForge.Init.Binder.cs @@ -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), }; } } diff --git a/MycroForge.CLI/Commands/MycroForge.Init.Options.cs b/MycroForge.CLI/Commands/MycroForge.Init.Options.cs index fcd23ef..06de7ee 100644 --- a/MycroForge.CLI/Commands/MycroForge.Init.Options.cs +++ b/MycroForge.CLI/Commands/MycroForge.Init.Options.cs @@ -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 }; } } diff --git a/MycroForge.CLI/Commands/MycroForge.Init.cs b/MycroForge.CLI/Commands/MycroForge.Init.cs index 3a1dc6c..697df05 100644 --- a/MycroForge.CLI/Commands/MycroForge.Init.cs +++ b/MycroForge.CLI/Commands/MycroForge.Init.cs @@ -39,6 +39,11 @@ public partial class MycroForge description: "The database UI port" ); + private static readonly Option DbuPlatformOption = new( + aliases: ["--database-ui-platform", "--dbu-platform"], + description: "The docker platform for the PhpMyAdmin image" + ); + private readonly ProjectContext _context; private readonly List _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()); } diff --git a/MycroForge.CLI/Extensions/StringExtensions.cs b/MycroForge.CLI/Extensions/StringExtensions.cs index a75c322..daf84c6 100644 --- a/MycroForge.CLI/Extensions/StringExtensions.cs +++ b/MycroForge.CLI/Extensions/StringExtensions.cs @@ -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(); } \ No newline at end of file diff --git a/MycroForge.CLI/Features/Db.Options.cs b/MycroForge.CLI/Features/Db.Options.cs index d8aa6d3..26836fa 100644 --- a/MycroForge.CLI/Features/Db.Options.cs +++ b/MycroForge.CLI/Features/Db.Options.cs @@ -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; } } } \ No newline at end of file diff --git a/MycroForge.CLI/Features/Db.cs b/MycroForge.CLI/Features/Db.cs index 0f3864b..4b8ba64 100644 --- a/MycroForge.CLI/Features/Db.cs +++ b/MycroForge.CLI/Features/Db.cs @@ -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(); 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); } diff --git a/MycroForge.CLI/scripts/publish-nuget.sh b/MycroForge.CLI/scripts/publish-nuget.sh index 25ae9c7..8fa3ec0 100644 --- a/MycroForge.CLI/scripts/publish-nuget.sh +++ b/MycroForge.CLI/scripts/publish-nuget.sh @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/usr/bin/bash dotnet pack -v d diff --git a/MycroForge.Core/Attributes/DockerPlatformAttribute.cs b/MycroForge.Core/Attributes/DockerPlatformAttribute.cs new file mode 100644 index 0000000..42fa2e1 --- /dev/null +++ b/MycroForge.Core/Attributes/DockerPlatformAttribute.cs @@ -0,0 +1,6 @@ +namespace MycroForge.Core.Attributes; + +public class DockerPlatformAttribute : Attribute +{ + public string Platform { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/MycroForge.Core/Extensions/EnumExtensions.cs b/MycroForge.Core/Extensions/EnumExtensions.cs new file mode 100644 index 0000000..3b399c4 --- /dev/null +++ b/MycroForge.Core/Extensions/EnumExtensions.cs @@ -0,0 +1,19 @@ +using System.Reflection; +using MycroForge.Core.Attributes; + +namespace MycroForge.Core.Extensions; + +public static class EnumExtensions +{ + /// + /// A generic extension method that aids in reflecting + /// and retrieving any attribute that is applied to an `Enum`. + /// + public static string ToDockerPlatformString(this ProjectConfig.DbConfig.DbuPlatformOptions value) + { + return value.GetType() + .GetMember(value.ToString()) + .FirstOrDefault()! + .GetCustomAttribute()!.Platform; + } +} \ No newline at end of file diff --git a/MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs b/MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs new file mode 100644 index 0000000..2cd9206 --- /dev/null +++ b/MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs @@ -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 + } + } +} \ No newline at end of file diff --git a/MycroForge.Core/ProjectConfig.DbConfig.cs b/MycroForge.Core/ProjectConfig.DbConfig.cs index c3f0e35..104c07d 100644 --- a/MycroForge.Core/ProjectConfig.DbConfig.cs +++ b/MycroForge.Core/ProjectConfig.DbConfig.cs @@ -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; } } } \ No newline at end of file diff --git a/README.md b/README.md index d36d06e..9537622 100644 --- a/README.md +++ b/README.md @@ -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 -``` \ No newline at end of file +``` + +### 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 +- \ No newline at end of file diff --git a/docs/docs/Commands/m4g_add/m4g_add_db.md b/docs/docs/Commands/m4g_add/m4g_add_db.md index 7735ef2..2790b7c 100644 --- a/docs/docs/Commands/m4g_add/m4g_add_db.md +++ b/docs/docs/Commands/m4g_add/m4g_add_db.md @@ -12,7 +12,8 @@ Usage: m4g add db [options] Options: - --database-host-port, --dbh-port The database host port - --database-ui-port, --dbu-port The database UI port - -?, -h, --help Show help and usage information + --database-host-port, --dbh-port The database host port + --database-ui-port, --dbu-port The database UI port + --database-ui-platform, --dbu-platform The docker platform for the PhpMyAdmin image + -?, -h, --help Show help and usage information ``` \ No newline at end of file diff --git a/docs/docs/Commands/m4g_init.md b/docs/docs/Commands/m4g_init.md index 0eaadea..ef85f7c 100644 --- a/docs/docs/Commands/m4g_init.md +++ b/docs/docs/Commands/m4g_init.md @@ -15,9 +15,10 @@ Arguments: The name of your project Options: - --without Features to exclude - --api-port The API port - --database-host-port, --dbh-port The database host port - --database-ui-port, --dbu-port The database UI port - -?, -h, --help Show help and usage information + --without Features to exclude + --api-port The API port + --database-host-port, --dbh-port The database host port + --database-ui-port, --dbu-port The database UI port + --database-ui-platform, --dbu-platform The docker platform for the PhpMyAdmin image + -?, -h, --help Show help and usage information ``` diff --git a/docs/docs/getting_started.md b/docs/docs/getting_started.md index 6178f43..9ca20e7 100644 --- a/docs/docs/getting_started.md +++ b/docs/docs/getting_started.md @@ -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. \ No newline at end of file diff --git a/docs/docs/intro.md b/docs/docs/intro.md index e886c30..f5cd91a 100644 --- a/docs/docs/intro.md +++ b/docs/docs/intro.md @@ -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! diff --git a/docs/docs/tutorial.md b/docs/docs/tutorial.md index 7d6b1b2..bae9e75 100644 --- a/docs/docs/tutorial.md +++ b/docs/docs/tutorial.md @@ -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! \ No newline at end of file