Cleaned up project and added constraints for SQLAlchemy types in generate entity
This commit is contained in:
parent
91431fd996
commit
32b7a3c01c
@ -96,14 +96,18 @@ public partial class EntityLinker
|
|||||||
var left = await LoadEntity(_left);
|
var left = await LoadEntity(_left);
|
||||||
var right = await LoadEntity(_right);
|
var right = await LoadEntity(_right);
|
||||||
|
|
||||||
var associationTable = string.Join('\n', AssociationTable);
|
var associationTable = string.Join('\n', AssociationTable)
|
||||||
associationTable = associationTable
|
|
||||||
.Replace("%left_entity%", left.ClassName.Underscore().ToLower())
|
.Replace("%left_entity%", left.ClassName.Underscore().ToLower())
|
||||||
.Replace("%right_entity%", right.ClassName.Underscore().ToLower())
|
.Replace("%right_entity%", right.ClassName.Underscore().ToLower())
|
||||||
.Replace("%left_table%", left.TableName)
|
.Replace("%left_table%", left.TableName)
|
||||||
.Replace("%right_table%", right.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);
|
await _context.CreateFile(associationTablePath, associationTable);
|
||||||
|
|
||||||
@ -136,21 +140,25 @@ public partial class EntityLinker
|
|||||||
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
|
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
|
||||||
env = new DbEnvModifier(env, associationTableImportPath, associationTableImportName).Rewrite();
|
env = new DbEnvModifier(env, associationTableImportPath, associationTableImportName).Rewrite();
|
||||||
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
|
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
|
||||||
|
|
||||||
var main = await _context.ReadFile("main.py");
|
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);
|
await _context.WriteFile("main.py", main);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<EntityModel> LoadEntity(string name)
|
private async Task<EntityModel> LoadEntity(string name)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(name);
|
var fqn = new FullyQualifiedName(name);
|
||||||
var path = $"{Features.Db.FeatureName}/entities";
|
var path = Path.Join(Features.Db.FeatureName, "entities");
|
||||||
|
|
||||||
if (fqn.HasNamespace)
|
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));
|
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));
|
||||||
entity.Initialize();
|
entity.Initialize();
|
||||||
return entity;
|
return entity;
|
||||||
|
@ -7,14 +7,16 @@ namespace MycroForge.CLI.CodeGen;
|
|||||||
|
|
||||||
public class RequestClassGenerator
|
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 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 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
|
public enum Type
|
||||||
{
|
{
|
||||||
@ -47,7 +49,7 @@ public class RequestClassGenerator
|
|||||||
var entitySource = await _context.ReadFile(entityFilePath);
|
var entitySource = await _context.ReadFile(entityFilePath);
|
||||||
var fieldInfo = ReadFields(entitySource);
|
var fieldInfo = ReadFields(entitySource);
|
||||||
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
||||||
|
|
||||||
var requestFilePath = Path.Join(
|
var requestFilePath = Path.Join(
|
||||||
Features.Api.FeatureName,
|
Features.Api.FeatureName,
|
||||||
"requests",
|
"requests",
|
||||||
@ -100,10 +102,11 @@ public class RequestClassGenerator
|
|||||||
.Replace(" ", "");
|
.Replace(" ", "");
|
||||||
Console.WriteLine(str); // = "List,Dict,str,Any"
|
Console.WriteLine(str); // = "List,Dict,str,Any"
|
||||||
*/
|
*/
|
||||||
var dissectedTypes = field.Type.Replace("[", ",")
|
var dissectedTypes = field.Type
|
||||||
|
.Replace("[", ",")
|
||||||
.Replace("]", "")
|
.Replace("]", "")
|
||||||
.Replace(" ", "")
|
.Replace(" ", "")
|
||||||
.Split();
|
.Split(',');
|
||||||
|
|
||||||
foreach (var dissectedType in dissectedTypes)
|
foreach (var dissectedType in dissectedTypes)
|
||||||
{
|
{
|
||||||
@ -164,16 +167,16 @@ public class RequestClassGenerator
|
|||||||
.Split(',')
|
.Split(',')
|
||||||
.Select(s => s.Trim())
|
.Select(s => s.Trim())
|
||||||
.ToArray();
|
.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)
|
if (imports.FirstOrDefault(i => i.Name == "typing") is Import typingImport)
|
||||||
{
|
{
|
||||||
typingImport.Types.AddRange(["Any", "Dict", "List", "Optional"]);
|
typingImport.Types.AddRange(PythonTypingImports);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
imports.Add(new("typing", ["Any", "Dict", "List", "Optional"]));
|
imports.Add(new Import("typing", PythonTypingImports));
|
||||||
}
|
}
|
||||||
|
|
||||||
return imports;
|
return imports;
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
@ -43,16 +43,16 @@ public partial class MycroForge
|
|||||||
private async Task ExecuteAsync(string name)
|
private async Task ExecuteAsync(string name)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(name);
|
var fqn = new FullyQualifiedName(name);
|
||||||
var routersFolderPath = $"{Features.Api.FeatureName}/routers";
|
var routersFolderPath = Path.Join(Features.Api.FeatureName, "routers");
|
||||||
|
|
||||||
if (fqn.HasNamespace)
|
if (fqn.HasNamespace)
|
||||||
routersFolderPath = Path.Combine(routersFolderPath, fqn.Namespace);
|
routersFolderPath = Path.Join(routersFolderPath, fqn.Namespace);
|
||||||
|
|
||||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||||
var filePath = Path.Combine(routersFolderPath, fileName);
|
var filePath = Path.Join(routersFolderPath, fileName);
|
||||||
|
|
||||||
await _context.CreateFile(filePath, Template);
|
await _context.CreateFile(filePath, Template);
|
||||||
|
|
||||||
var moduleImportPath = routersFolderPath
|
var moduleImportPath = routersFolderPath
|
||||||
.Replace('\\', '.')
|
.Replace('\\', '.')
|
||||||
.Replace('/', '.');
|
.Replace('/', '.');
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using MycroForge.CLI.CodeGen;
|
using MycroForge.CLI.CodeGen;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
@ -14,11 +15,65 @@ public partial class MycroForge
|
|||||||
{
|
{
|
||||||
public class Entity : Command, ISubCommandOf<Generate>
|
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 =
|
private static readonly string[] Template =
|
||||||
[
|
[
|
||||||
"from sqlalchemy import %type_imports%",
|
"from sqlalchemy import %sqlalchemy_imports%",
|
||||||
"from sqlalchemy.orm import Mapped, mapped_column",
|
"from sqlalchemy.orm import Mapped, mapped_column",
|
||||||
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
||||||
"",
|
"",
|
||||||
@ -55,6 +110,10 @@ public partial class MycroForge
|
|||||||
"\tfirst_name:str:String(255)",
|
"\tfirst_name:str:String(255)",
|
||||||
])) { AllowMultipleArgumentsPerToken = true };
|
])) { AllowMultipleArgumentsPerToken = true };
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private record ColumnDefinition(string Name, string NativeType, string SqlAlchemyType);
|
||||||
|
|
||||||
private readonly ProjectContext _context;
|
private readonly ProjectContext _context;
|
||||||
|
|
||||||
public Entity(ProjectContext context) : base("entity", "Generate and database entity")
|
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<string> columns)
|
private async Task ExecuteAsync(string name, IEnumerable<string> columns)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(name);
|
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)
|
if (fqn.HasNamespace)
|
||||||
folderPath = Path.Combine(folderPath, fqn.Namespace);
|
folderPath = Path.Join(folderPath, fqn.Namespace);
|
||||||
|
|
||||||
var _columns = GetColumnDefinitions(columns.ToArray());
|
var sqlAlchemyColumn = GetColumnDefinitions(columns.ToArray());
|
||||||
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());
|
var distinctSqlAlchemyColumnTypes = sqlAlchemyColumn
|
||||||
var columnDefinitions = string.Join("\n\t", _columns.Select(ColumnToString));
|
.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);
|
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("%class_name%", fqn.PascalizedName);
|
||||||
code = code.Replace("%table_name%", fqn.SnakeCasedName.Pluralize());
|
code = code.Replace("%table_name%", fqn.SnakeCasedName.Pluralize());
|
||||||
code = code.Replace("%column_definitions%", columnDefinitions);
|
code = code.Replace("%column_definitions%", columnDefinitions);
|
||||||
|
|
||||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||||
var filePath = Path.Combine(folderPath, fileName);
|
var filePath = Path.Join(folderPath, fileName);
|
||||||
await _context.CreateFile(filePath, code);
|
await _context.CreateFile(filePath, code);
|
||||||
|
|
||||||
var importPathParts = new[] { folderPath, fileName.Replace(".py", "") }
|
var importPathParts = new[] { folderPath, fileName.Replace(".py", "") }
|
||||||
@ -108,25 +169,58 @@ public partial class MycroForge
|
|||||||
await _context.WriteFile("main.py", main);
|
await _context.WriteFile("main.py", main);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ColumnDefinition> GetColumnDefinitions(string[] fields)
|
private List<ColumnDefinition> GetColumnDefinitions(string[] columns)
|
||||||
{
|
{
|
||||||
var definitions = new List<ColumnDefinition>();
|
var definitions = new List<ColumnDefinition>();
|
||||||
|
|
||||||
foreach (var field in fields)
|
foreach (var column in columns)
|
||||||
{
|
{
|
||||||
if (field.Split(':') is not { Length: 3 } definition)
|
if (column.Split(':') is not { Length: 3 } definition)
|
||||||
throw new Exception($"Field definition {field} is invalid.");
|
throw new Exception($"Column definition {column} is invalid.");
|
||||||
|
|
||||||
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateSqlAlchemyColumnTypes(definitions);
|
||||||
|
|
||||||
return 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})";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,9 +59,9 @@ public partial class MycroForge
|
|||||||
var folderPath = string.Empty;
|
var folderPath = string.Empty;
|
||||||
|
|
||||||
if (fqn.HasNamespace)
|
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 template = withSession ? WithSessionTemplate : DefaultTemplate;
|
||||||
var code = string.Join('\n', template)
|
var code = string.Join('\n', template)
|
||||||
.Replace("%class_name%", fqn.PascalizedName);
|
.Replace("%class_name%", fqn.PascalizedName);
|
||||||
|
@ -81,7 +81,7 @@ public partial class MycroForge
|
|||||||
await _context.CreateFile("main.py");
|
await _context.CreateFile("main.py");
|
||||||
|
|
||||||
// Create the venv
|
// 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
|
// Pass feature arguments to the ArgsContainer
|
||||||
_optionsContainer.Set(options.ApiOptions);
|
_optionsContainer.Set(options.ApiOptions);
|
||||||
@ -106,7 +106,7 @@ public partial class MycroForge
|
|||||||
|
|
||||||
private async Task<string> CreateDirectory(string name)
|
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))
|
if (Directory.Exists(directory))
|
||||||
throw new Exception($"Directory {directory} already exists.");
|
throw new Exception($"Directory {directory} already exists.");
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
|||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresVenv]
|
||||||
public class Install : Command, ISubCommandOf<MycroForge>
|
public class Install : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
|||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresVenv]
|
||||||
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||||
|
@ -48,6 +48,9 @@ public static class CommandExtensions
|
|||||||
|
|
||||||
else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute)
|
else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute)
|
||||||
requiresPluginAttribute.RequirePluginProject(commandText);
|
requiresPluginAttribute.RequirePluginProject(commandText);
|
||||||
|
|
||||||
|
else if (_command.GetRequiresVenvAttribute() is RequiresVenvAttribute requiresVenvAttribute)
|
||||||
|
requiresVenvAttribute.RequireVenv(commandText);
|
||||||
}
|
}
|
||||||
|
|
||||||
await next(context);
|
await next(context);
|
||||||
@ -56,15 +59,6 @@ public static class CommandExtensions
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 List<Command> GetCommandChain(this InvocationContext context)
|
private static List<Command> GetCommandChain(this InvocationContext context)
|
||||||
{
|
{
|
||||||
var chain = new List<Command>();
|
var chain = new List<Command>();
|
||||||
@ -87,4 +81,16 @@ public static class CommandExtensions
|
|||||||
|
|
||||||
return chain;
|
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>();
|
||||||
}
|
}
|
@ -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<RootCommand>
|
|
||||||
{
|
|
||||||
private readonly Argument<string> NameArgument =
|
|
||||||
new(name: "name", description: "The name of the person to greet");
|
|
||||||
|
|
||||||
private readonly Option<bool> 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!"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<ISubCommandOf<RootCommand>, HelloWorldCommand>();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Include=".template.config\template.json" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="MycroForge.Core" Version="1.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
@ -1,6 +0,0 @@
|
|||||||
namespace MycroForge.Core.Attributes;
|
|
||||||
|
|
||||||
public class DockerPlatformAttribute : Attribute
|
|
||||||
{
|
|
||||||
public string Platform { get; set; } = string.Empty;
|
|
||||||
}
|
|
@ -9,7 +9,7 @@ public class ProjectContext
|
|||||||
{
|
{
|
||||||
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
||||||
public string AppName => Path.GetFileNameWithoutExtension(RootDirectory).Underscore().ToLower();
|
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)
|
public async Task<ProjectConfig> LoadConfig(bool create = false)
|
||||||
@ -37,21 +37,9 @@ public class ProjectContext
|
|||||||
RootDirectory = path;
|
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)
|
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);
|
var fileInfo = new FileInfo(fullPath);
|
||||||
|
|
||||||
if (fileInfo.Exists) return;
|
if (fileInfo.Exists) return;
|
||||||
@ -64,7 +52,7 @@ public class ProjectContext
|
|||||||
|
|
||||||
public async Task<string> ReadFile(string 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);
|
var fileInfo = new FileInfo(fullPath);
|
||||||
|
|
||||||
if (!fileInfo.Exists)
|
if (!fileInfo.Exists)
|
||||||
@ -75,7 +63,7 @@ public class ProjectContext
|
|||||||
|
|
||||||
public async Task WriteFile(string path, params string[] content)
|
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);
|
var fileInfo = new FileInfo(fullPath);
|
||||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||||
@ -123,7 +111,6 @@ public class ProjectContext
|
|||||||
input.Close();
|
input.Close();
|
||||||
|
|
||||||
await process.WaitForExitAsync();
|
await process.WaitForExitAsync();
|
||||||
|
|
||||||
Environment.ExitCode = process.ExitCode;
|
Environment.ExitCode = process.ExitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,8 +30,8 @@ dotnet nuget add source --name devdisciples --username username --password passw
|
|||||||
|
|
||||||
### TODO
|
### TODO
|
||||||
|
|
||||||
- Fix `-c` option for `m4g db generate entity`
|
|
||||||
- Add a CLI UI library
|
- Add a CLI UI library
|
||||||
- Clean up README files
|
- Clean up README files
|
||||||
- Theme the site with a custom color scheme and icon/logos
|
- Theme the site with a custom color scheme and icon/logos
|
||||||
-
|
- Mention assumed knowledge of both FastAPI & SQLAlchemy
|
||||||
|
- Elaborate on terminology and best practices, like meaningful service names (also emphasize common sense?)
|
||||||
|
Loading…
Reference in New Issue
Block a user