From 32b7a3c01cf65724188f506058fa4b1c4ad7212e Mon Sep 17 00:00:00 2001 From: mdnapo Date: Wed, 24 Jul 2024 07:18:42 +0200 Subject: [PATCH] Cleaned up project and added constraints for SQLAlchemy types in generate entity --- MycroForge.CLI/CodeGen/EntityLinker.cs | 26 ++-- .../CodeGen/RequestClassGenerator.cs | 19 +-- .../Attributes/RequiresVenvAttribute.cs | 12 ++ .../MycroForge.Api.Generate.Router.cs | 10 +- .../Commands/MycroForge.Db.Generate.Entity.cs | 130 +++++++++++++++--- .../Commands/MycroForge.Generate.Service.cs | 4 +- MycroForge.CLI/Commands/MycroForge.Init.cs | 4 +- MycroForge.CLI/Commands/MycroForge.Install.cs | 2 + .../Commands/MycroForge.Uninstall.cs | 2 + .../Extensions/CommandExtensions.cs | 24 ++-- MycroForge.CLI/My.Plugin/HelloWorldCommand.cs | 36 ----- .../My.Plugin/HelloWorldCommandPlugin.cs | 15 -- MycroForge.CLI/My.Plugin/My.Plugin.csproj | 17 --- .../Attributes/DockerPlatformAttribute.cs | 6 - MycroForge.Core/ProjectContext.cs | 21 +-- README.md | 4 +- 16 files changed, 186 insertions(+), 146 deletions(-) create mode 100644 MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs delete mode 100644 MycroForge.CLI/My.Plugin/HelloWorldCommand.cs delete mode 100644 MycroForge.CLI/My.Plugin/HelloWorldCommandPlugin.cs delete mode 100644 MycroForge.CLI/My.Plugin/My.Plugin.csproj delete mode 100644 MycroForge.Core/Attributes/DockerPlatformAttribute.cs diff --git a/MycroForge.CLI/CodeGen/EntityLinker.cs b/MycroForge.CLI/CodeGen/EntityLinker.cs index a23d28b..bd25000 100644 --- a/MycroForge.CLI/CodeGen/EntityLinker.cs +++ b/MycroForge.CLI/CodeGen/EntityLinker.cs @@ -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 LoadEntity(string name) { var fqn = new FullyQualifiedName(name); - var path = $"{Features.Db.FeatureName}/entities"; + var path = Path.Join(Features.Db.FeatureName, "entities"); if (fqn.HasNamespace) - path = Path.Combine(path, fqn.Namespace); + 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; diff --git a/MycroForge.CLI/CodeGen/RequestClassGenerator.cs b/MycroForge.CLI/CodeGen/RequestClassGenerator.cs index 2abb40f..f6e78b5 100644 --- a/MycroForge.CLI/CodeGen/RequestClassGenerator.cs +++ b/MycroForge.CLI/CodeGen/RequestClassGenerator.cs @@ -7,14 +7,16 @@ namespace MycroForge.CLI.CodeGen; public class RequestClassGenerator { - public record Import(string Name, List Types) + private static readonly List PythonTypingImports = ["Any", "Dict", "List", "Optional"]; + + private record Import(string Name, List 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 { @@ -47,7 +49,7 @@ public class RequestClassGenerator var entitySource = await _context.ReadFile(entityFilePath); var fieldInfo = ReadFields(entitySource); var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type))); - + var requestFilePath = Path.Join( Features.Api.FeatureName, "requests", @@ -100,10 +102,11 @@ 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) { @@ -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(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; diff --git a/MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs b/MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs new file mode 100644 index 0000000..ba37bd0 --- /dev/null +++ b/MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs @@ -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}"); + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs index 659038f..1e8a912 100644 --- a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs +++ b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs @@ -43,16 +43,16 @@ public partial class MycroForge private async Task ExecuteAsync(string name) { var fqn = new FullyQualifiedName(name); - var routersFolderPath = $"{Features.Api.FeatureName}/routers"; + var routersFolderPath = Path.Join(Features.Api.FeatureName, "routers"); if (fqn.HasNamespace) - routersFolderPath = Path.Combine(routersFolderPath, fqn.Namespace); - + routersFolderPath = Path.Join(routersFolderPath, fqn.Namespace); + var fileName = $"{fqn.SnakeCasedName}.py"; - var filePath = Path.Combine(routersFolderPath, fileName); + var filePath = Path.Join(routersFolderPath, fileName); await _context.CreateFile(filePath, Template); - + var moduleImportPath = routersFolderPath .Replace('\\', '.') .Replace('/', '.'); diff --git a/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs b/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs index fa08287..7467502 100644 --- a/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs +++ b/MycroForge.CLI/Commands/MycroForge.Db.Generate.Entity.cs @@ -1,4 +1,5 @@ using System.CommandLine; +using System.Text.RegularExpressions; using Humanizer; using MycroForge.CLI.CodeGen; using MycroForge.Core.Contract; @@ -14,11 +15,65 @@ public partial class MycroForge { public class Entity : Command, ISubCommandOf { - 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", "", @@ -55,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") @@ -69,25 +128,27 @@ public partial class MycroForge private async Task ExecuteAsync(string name, IEnumerable 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.Combine(folderPath, fqn.Namespace); + folderPath = Path.Join(folderPath, fqn.Namespace); - 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 sqlAlchemyColumn = GetColumnDefinitions(columns.ToArray()); + var distinctSqlAlchemyColumnTypes = sqlAlchemyColumn + .Select(c => c.SqlAlchemyType.Split('(').First()) + .Distinct(); + + 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", "") } @@ -108,25 +169,58 @@ public partial class MycroForge await _context.WriteFile("main.py", main); } - private List GetColumnDefinitions(string[] fields) + private List GetColumnDefinitions(string[] columns) { var definitions = new List(); - 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 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})"; } } } diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs index 0d4277b..1548c2e 100644 --- a/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs +++ b/MycroForge.CLI/Commands/MycroForge.Generate.Service.cs @@ -59,9 +59,9 @@ public partial class MycroForge var folderPath = string.Empty; if (fqn.HasNamespace) - folderPath = Path.Combine(folderPath, fqn.Namespace); + 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); diff --git a/MycroForge.CLI/Commands/MycroForge.Init.cs b/MycroForge.CLI/Commands/MycroForge.Init.cs index 697df05..8d8569f 100644 --- a/MycroForge.CLI/Commands/MycroForge.Init.cs +++ b/MycroForge.CLI/Commands/MycroForge.Init.cs @@ -81,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); @@ -106,7 +106,7 @@ public partial class MycroForge private async Task 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."); diff --git a/MycroForge.CLI/Commands/MycroForge.Install.cs b/MycroForge.CLI/Commands/MycroForge.Install.cs index f50aa75..0821850 100644 --- a/MycroForge.CLI/Commands/MycroForge.Install.cs +++ b/MycroForge.CLI/Commands/MycroForge.Install.cs @@ -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 { private static readonly Argument> PackagesArgument = diff --git a/MycroForge.CLI/Commands/MycroForge.Uninstall.cs b/MycroForge.CLI/Commands/MycroForge.Uninstall.cs index cb62329..a777637 100644 --- a/MycroForge.CLI/Commands/MycroForge.Uninstall.cs +++ b/MycroForge.CLI/Commands/MycroForge.Uninstall.cs @@ -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 { private static readonly Argument> PackagesArgument = diff --git a/MycroForge.CLI/Extensions/CommandExtensions.cs b/MycroForge.CLI/Extensions/CommandExtensions.cs index dcde067..7cb7b5e 100644 --- a/MycroForge.CLI/Extensions/CommandExtensions.cs +++ b/MycroForge.CLI/Extensions/CommandExtensions.cs @@ -48,6 +48,9 @@ public static class CommandExtensions else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute) requiresPluginAttribute.RequirePluginProject(commandText); + + else if (_command.GetRequiresVenvAttribute() is RequiresVenvAttribute requiresVenvAttribute) + requiresVenvAttribute.RequireVenv(commandText); } await next(context); @@ -56,15 +59,6 @@ public static class CommandExtensions return builder; } - private static RequiresFeatureAttribute? GetRequiresFeatureAttribute(this Command command) => - command.GetType().GetCustomAttribute(); - - private static RequiresFileAttribute? GetRequiresFileAttribute(this Command command) => - command.GetType().GetCustomAttribute(); - - private static RequiresPluginAttribute? GetRequiresPluginAttribute(this Command command) => - command.GetType().GetCustomAttribute(); - private static List GetCommandChain(this InvocationContext context) { var chain = new List(); @@ -87,4 +81,16 @@ public static class CommandExtensions return chain; } + + private static RequiresFeatureAttribute? GetRequiresFeatureAttribute(this Command command) => + command.GetType().GetCustomAttribute(); + + private static RequiresFileAttribute? GetRequiresFileAttribute(this Command command) => + command.GetType().GetCustomAttribute(); + + private static RequiresPluginAttribute? GetRequiresPluginAttribute(this Command command) => + command.GetType().GetCustomAttribute(); + + private static RequiresVenvAttribute? GetRequiresVenvAttribute(this Command command) => + command.GetType().GetCustomAttribute(); } \ No newline at end of file diff --git a/MycroForge.CLI/My.Plugin/HelloWorldCommand.cs b/MycroForge.CLI/My.Plugin/HelloWorldCommand.cs deleted file mode 100644 index 281b5dc..0000000 --- a/MycroForge.CLI/My.Plugin/HelloWorldCommand.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.CommandLine; -using MycroForge.Core; -using MycroForge.Core.Contract; -using RootCommand = MycroForge.Core.RootCommand; - -namespace My.Plugin; - -public class HelloWorldCommand : Command, ISubCommandOf -{ - private readonly Argument NameArgument = - new(name: "name", description: "The name of the person to greet"); - - private readonly Option AllCapsOption = - new(aliases: ["-a", "--all-caps"], description: "Print the name in all caps"); - - private readonly ProjectContext _context; - - public HelloWorldCommand(ProjectContext context) : - base("hello", "An example command generated by dotnet new using the m4gp template") - { - _context = context; - AddArgument(NameArgument); - AddOption(AllCapsOption); - this.SetHandler(ExecuteAsync, NameArgument, AllCapsOption); - } - - private async Task ExecuteAsync(string name, bool allCaps) - { - name = allCaps ? name.ToUpper() : name; - - await _context.CreateFile("hello_world.txt", - $"Hello {name}!", - "This file was generated by your custom command!" - ); - } -} \ No newline at end of file diff --git a/MycroForge.CLI/My.Plugin/HelloWorldCommandPlugin.cs b/MycroForge.CLI/My.Plugin/HelloWorldCommandPlugin.cs deleted file mode 100644 index adbecfa..0000000 --- a/MycroForge.CLI/My.Plugin/HelloWorldCommandPlugin.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using MycroForge.Core.Contract; -using RootCommand = MycroForge.Core.RootCommand; - -namespace My.Plugin; - -public class HelloWorldCommandPlugin : ICommandPlugin -{ - public string Name => "My.Plugin"; - - public void RegisterServices(IServiceCollection services) - { - services.AddScoped, HelloWorldCommand>(); - } -} \ No newline at end of file diff --git a/MycroForge.CLI/My.Plugin/My.Plugin.csproj b/MycroForge.CLI/My.Plugin/My.Plugin.csproj deleted file mode 100644 index 4943c9c..0000000 --- a/MycroForge.CLI/My.Plugin/My.Plugin.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - net8.0 - enable - enable - - - - - - - - - - - diff --git a/MycroForge.Core/Attributes/DockerPlatformAttribute.cs b/MycroForge.Core/Attributes/DockerPlatformAttribute.cs deleted file mode 100644 index 42fa2e1..0000000 --- a/MycroForge.Core/Attributes/DockerPlatformAttribute.cs +++ /dev/null @@ -1,6 +0,0 @@ -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/ProjectContext.cs b/MycroForge.Core/ProjectContext.cs index 4269464..ae93142 100644 --- a/MycroForge.Core/ProjectContext.cs +++ b/MycroForge.Core/ProjectContext.cs @@ -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 LoadConfig(bool create = false) @@ -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; @@ -64,7 +52,7 @@ public class ProjectContext public async Task ReadFile(string path) { - var fullPath = Path.Combine(RootDirectory, path); + var fullPath = Path.Join(RootDirectory, path); var fileInfo = new FileInfo(fullPath); if (!fileInfo.Exists) @@ -75,7 +63,7 @@ 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)); @@ -123,7 +111,6 @@ public class ProjectContext input.Close(); await process.WaitForExitAsync(); - Environment.ExitCode = process.ExitCode; } diff --git a/README.md b/README.md index 5e64def..8503b76 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ dotnet nuget add source --name devdisciples --username username --password passw ### TODO -- Fix `-c` option for `m4g db generate entity` - Add a CLI UI library - Clean up README files - Theme the site with a custom color scheme and icon/logos -- \ No newline at end of file +- Mention assumed knowledge of both FastAPI & SQLAlchemy +- Elaborate on terminology and best practices, like meaningful service names (also emphasize common sense?)