Cleaned up project and added constraints for SQLAlchemy types in generate entity

This commit is contained in:
2024-07-24 07:18:42 +02:00
parent 91431fd996
commit 32b7a3c01c
16 changed files with 186 additions and 146 deletions

View 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}");
}
}

View File

@@ -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('/', '.');

View File

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

View File

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

View File

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

View File

@@ -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 =

View File

@@ -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 =