Cleaned up project and added constraints for SQLAlchemy types in generate entity
This commit is contained in:
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)
|
||||
{
|
||||
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('/', '.');
|
||||
|
||||
@@ -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<Generate>
|
||||
{
|
||||
private record ColumnDefinition(string Name, string NativeType, string OrmType);
|
||||
#region Hidden region
|
||||
|
||||
private static string[] SqlAlchemyTypes =
|
||||
[
|
||||
"BigInteger",
|
||||
"Boolean",
|
||||
"Date",
|
||||
"DateTime",
|
||||
"Enum",
|
||||
"Double",
|
||||
"Float",
|
||||
"Integer",
|
||||
"Interval",
|
||||
"LargeBinary",
|
||||
"MatchType",
|
||||
"Numeric",
|
||||
"PickleType",
|
||||
"SchemaType",
|
||||
"SmallInteger",
|
||||
"String",
|
||||
"Text",
|
||||
"Time",
|
||||
"Unicode",
|
||||
"UnicodeText",
|
||||
"Uuid",
|
||||
"ARRAY",
|
||||
"BIGINT",
|
||||
"BINARY",
|
||||
"BLOB",
|
||||
"BOOLEAN",
|
||||
"CHAR",
|
||||
"CLOB",
|
||||
"DATE",
|
||||
"DATETIME",
|
||||
"DECIMAL",
|
||||
"DOUBLE",
|
||||
"DOUBLE_PRECISION",
|
||||
"FLOAT",
|
||||
"INT",
|
||||
"JSON",
|
||||
"INTEGER",
|
||||
"NCHAR",
|
||||
"NVARCHAR",
|
||||
"NUMERIC",
|
||||
"REAL",
|
||||
"SMALLINT",
|
||||
"TEXT",
|
||||
"TIME",
|
||||
"TIMESTAMP",
|
||||
"UUID",
|
||||
"VARBINARY",
|
||||
"VARCHAR"
|
||||
];
|
||||
|
||||
private static readonly Regex SqlAlchemyTypeRegex = new(@".*\(.*\)");
|
||||
|
||||
private static readonly string[] Template =
|
||||
[
|
||||
"from sqlalchemy import %type_imports%",
|
||||
"from sqlalchemy import %sqlalchemy_imports%",
|
||||
"from sqlalchemy.orm import Mapped, mapped_column",
|
||||
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
||||
"",
|
||||
@@ -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<string> columns)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var folderPath = $"{Features.Db.FeatureName}/entities";
|
||||
var folderPath = Path.Join(Features.Db.FeatureName, "entities");
|
||||
|
||||
_context.AssertDirectoryExists(Features.Db.FeatureName);
|
||||
|
||||
if (fqn.HasNamespace)
|
||||
folderPath = Path.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<ColumnDefinition> GetColumnDefinitions(string[] fields)
|
||||
private List<ColumnDefinition> GetColumnDefinitions(string[] columns)
|
||||
{
|
||||
var definitions = new List<ColumnDefinition>();
|
||||
|
||||
foreach (var field in fields)
|
||||
foreach (var column in columns)
|
||||
{
|
||||
if (field.Split(':') is not { Length: 3 } definition)
|
||||
throw new Exception($"Field definition {field} is invalid.");
|
||||
if (column.Split(':') is not { Length: 3 } definition)
|
||||
throw new Exception($"Column definition {column} is invalid.");
|
||||
|
||||
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
||||
}
|
||||
|
||||
ValidateSqlAlchemyColumnTypes(definitions);
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
private static string ColumnToString(ColumnDefinition definition)
|
||||
private static void ValidateSqlAlchemyColumnTypes(List<ColumnDefinition> definitions)
|
||||
{
|
||||
return $"{definition.Name}: Mapped[{definition.NativeType}] = mapped_column({definition.OrmType})";
|
||||
foreach (var column in definitions)
|
||||
{
|
||||
if (!SqlAlchemyTypeRegex.IsMatch(column.SqlAlchemyType))
|
||||
{
|
||||
var message = new[]
|
||||
{
|
||||
$"SQLAlchemy column definition {column.SqlAlchemyType} was not properly defined.",
|
||||
"Add parentheses and specify parameters if required, an example is provided below.",
|
||||
" String(255)",
|
||||
"",
|
||||
"Available options are:",
|
||||
string.Join(Environment.NewLine, SqlAlchemyTypes.Select(type => $" - {type}"))
|
||||
};
|
||||
|
||||
throw new(string.Join(Environment.NewLine, message));
|
||||
}
|
||||
|
||||
var type = column.SqlAlchemyType.Split('(').First();
|
||||
|
||||
if (!SqlAlchemyTypes.Contains(type))
|
||||
{
|
||||
var message = string.Join(Environment.NewLine, [
|
||||
$"SQLAlchemy column type '{column.SqlAlchemyType}' is not valid, available options are:",
|
||||
string.Join(Environment.NewLine, SqlAlchemyTypes.Select(type => $" - {type}"))
|
||||
]);
|
||||
|
||||
throw new(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ColumnToString(ColumnDefinition definition) =>
|
||||
$"{definition.Name}: Mapped[{definition.NativeType}] = mapped_column({definition.SqlAlchemyType})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string> CreateDirectory(string name)
|
||||
{
|
||||
var directory = Path.Combine(Directory.GetCurrentDirectory(), name);
|
||||
var directory = Path.Join(Directory.GetCurrentDirectory(), name);
|
||||
|
||||
if (Directory.Exists(directory))
|
||||
throw new Exception($"Directory {directory} already exists.");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresVenv]
|
||||
public class Install : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresVenv]
|
||||
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
|
||||
Reference in New Issue
Block a user