diff --git a/MycroForge.CLI/CodeGen/EntityLinker.EntityModel.cs b/MycroForge.CLI/CodeGen/EntityLinker.EntityModel.cs new file mode 100644 index 0000000..e6bd746 --- /dev/null +++ b/MycroForge.CLI/CodeGen/EntityLinker.EntityModel.cs @@ -0,0 +1,132 @@ +using Humanizer; +using MycroForge.Parsing; + +namespace MycroForge.CLI.CodeGen; + +public partial class EntityLinker +{ + public class EntityModel : PythonSourceModifier + { + private readonly string _className; + private readonly string _path; + + private readonly List _importCtxs; + private readonly List _importsBuffer; + private PythonParser.Import_fromContext LastImport => _importCtxs.Last(); + + private readonly List _classCtxs; + private PythonParser.AssignmentContext _tableCtx; + + private readonly List _columnCtxs; + private readonly List _columnsBuffer; + private PythonParser.AssignmentContext LastColumn => _columnCtxs.Last(); + + public string ClassName => _className; + public string Path => _path; + public string FieldName => _className.ToLower().Underscore(); + + public string TableName => GetOriginalText(_tableCtx) + .Replace("__tablename__", string.Empty) + .Replace("=", string.Empty) + .Replace("\"", string.Empty) + .Trim(); + + public EntityModel(string className, string path, string source) : base(source) + { + _className = className; + _path = path; + _importCtxs = new(); + _importsBuffer = new(); + _classCtxs = new(); + _tableCtx = default!; + _columnCtxs = new(); + _columnsBuffer = new(); + } + + public void Initialize() + { + var tree = Parser.file_input(); + + Visit(tree); + + if (!_classCtxs.Any(c => GetOriginalText(c).Contains(_className))) + throw new Exception($"Entity {_className} was not found in {_path}."); + + if (_columnCtxs.Count == 0) + throw new Exception($"Entity {_className} has no columns."); + + _importsBuffer.Add(GetOriginalText(LastImport)); + _columnsBuffer.Add(GetOriginalText(LastColumn)); + + InsertRelationshipImport(); + InsertForeignKeyImport(); + } + + private void InsertRelationshipImport() + { + var relationship = _importCtxs.FirstOrDefault(import => + { + var text = GetOriginalText(import); + return text.Contains("sqlalchemy.orm") && text.Contains("relationship"); + }); + + if (relationship is null) + _importsBuffer.Add("from sqlalchemy.orm import relationship"); + } + + private void InsertForeignKeyImport() + { + var foreignKey = _importCtxs.FirstOrDefault(import => + { + var text = GetOriginalText(import); + return text.Contains("sqlalchemy") && text.Contains("ForeignKey"); + }); + + if (foreignKey is null) + _importsBuffer.Add("from sqlalchemy import ForeignKey"); + } + + public override object? VisitImport_from(PythonParser.Import_fromContext context) + { + _importCtxs.Add(context); + return base.VisitImport_from(context); + } + + public override object? VisitClass_def(PythonParser.Class_defContext context) + { + _classCtxs.Add(context); + return base.VisitClass_def(context); + } + + public override object? VisitAssignment(PythonParser.AssignmentContext context) + { + var text = GetOriginalText(context); + + if (text.StartsWith("__tablename__")) + _tableCtx = context; + + if (text.Contains("Mapped[")) + _columnCtxs.Add(context); + + return base.VisitAssignment(context); + } + + public void AppendColumn(params string[] text) => _columnsBuffer.Add(string.Join('\n', text)); + + public void AppendImportIfNotExists(string module, string import, params string[] text) + { + var isExisting = _importCtxs.Select(GetOriginalText).Any(ctx => ctx.Contains(module) && ctx.Contains(import)); + var isBuffered = _importsBuffer.Any(txt => txt.Contains(module) && txt.Contains(import)); + + if (!isExisting && !isBuffered) + _importsBuffer.Add(string.Join('\n', text)); + } + + public override string Rewrite() + { + Rewrite(LastImport, _importsBuffer.ToArray()); + Rewrite(LastColumn, _columnsBuffer.ToArray()); + return Rewriter.GetText(); + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/CodeGen/EntityLinker.cs b/MycroForge.CLI/CodeGen/EntityLinker.cs index 2f035b3..ad833e8 100644 --- a/MycroForge.CLI/CodeGen/EntityLinker.cs +++ b/MycroForge.CLI/CodeGen/EntityLinker.cs @@ -1,16 +1,136 @@ -using MycroForge.Parsing; +using Humanizer; namespace MycroForge.CLI.CodeGen; -public class EntityLinker : PythonSourceModifier +public partial class EntityLinker { - public EntityLinker(string source) : base(source) + private readonly ProjectContext _context; + private readonly string _left; + private readonly string _right; + + public EntityLinker(ProjectContext context, string left, string right) { + _context = context; + _left = left; + _right = right; } - public override object? VisitAssignment(PythonParser.AssignmentContext context) + public async Task OneToOne() { - Console.WriteLine(GetOriginalText(context)); - return base.VisitAssignment(context); + var left = await LoadEntity(_left); + var right = await LoadEntity(_right); + + left.AppendColumn([ + $"\t{right.FieldName}: Mapped[\"{right.ClassName}\"] = relationship(back_populates=\"{left.FieldName}\")" + ]); + + right.AppendColumn([ + $"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))", + $"\t{left.FieldName}: Mapped[\"{left.ClassName}\"] = relationship(back_populates=\"{right.FieldName}\")" + ]); + + await _context.WriteFile(left.Path, left.Rewrite()); + await _context.WriteFile(right.Path, right.Rewrite()); } -} + + public async Task OneToMany() + { + var left = await LoadEntity(_left); + var right = await LoadEntity(_right); + + left.AppendImportIfNotExists( + "typing", + "List", + "from typing import List" + ); + left.AppendColumn([ + $"\t{right.FieldName.Pluralize()}: Mapped[List[\"{right.ClassName}\"]] = relationship()" + ]); + + right.AppendColumn([ + $"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))", + $"\t{left.FieldName}: Mapped[\"{left.ClassName}\"] = relationship(back_populates=\"{right.FieldName}\")" + ]); + + await _context.WriteFile(left.Path, left.Rewrite()); + await _context.WriteFile(right.Path, right.Rewrite()); + } + + public async Task ManyToOne() + { + var left = await LoadEntity(_left); + var right = await LoadEntity(_right); + + left.AppendImportIfNotExists( + "typing", + "Optional", + "from typing import Optional" + ); + left.AppendColumn([ + $"\t{right.FieldName}_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"{right.TableName}.id\"))", + $"\t{right.FieldName}: Mapped[\"{right.ClassName}\"] = relationship(back_populates=\"{left.FieldName.Pluralize()}\")" + ]); + + left.AppendImportIfNotExists( + "typing", + "List", + "from typing import List" + ); + right.AppendColumn([ + $"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))", + $"\t{left.FieldName.Pluralize()}: Mapped[List[\"{left.ClassName}\"]] = relationship(back_populates=\"{right.FieldName}\")" + ]); + + await _context.WriteFile(left.Path, left.Rewrite()); + await _context.WriteFile(right.Path, right.Rewrite()); + } + + public async Task ManyToMany() + { + // var left = await LoadEntity(_left); + // var right = await LoadEntity(_right); + // + // left.AppendImportIfNotExists( + // $"orm.entities.{right.FieldName}", + // $"{right.ClassName}", + // $"from orm.entities.{right.FieldName} import {right.ClassName}" + // ); + // left.AppendImportIfNotExists( + // "typing", + // "Optional", + // "from typing import Optional" + // ); + // left.AppendColumn([ + // $"\t{right.FieldName}_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"{right.TableName}.id\"))", + // $"\t{right.FieldName}: Mapped[\"{right.ClassName}\"] = relationship(back_populates=\"{left.FieldName.Pluralize()}\")\n" + // ]); + // + // right.AppendImportIfNotExists( + // $"orm.entities.{left.FieldName}", + // $"{left.ClassName}", + // $"from orm.entities.{left.FieldName} import {left.ClassName}" + // ); + // left.AppendImportIfNotExists( + // "typing", + // "List", + // "from typing import List" + // ); + // right.AppendColumn([ + // $"\t{left.FieldName}_id: Mapped[int] = mapped_column(ForeignKey(\"{left.TableName}.id\"))", + // $"\t{left.FieldName.Pluralize()}: Mapped[List[\"{left.ClassName}\"]] = relationship(back_populates=\"{right.FieldName}\")\n" + // ]); + // + // await _context.WriteFile(left.Path, left.Rewrite()); + // await _context.WriteFile(right.Path, right.Rewrite()); + } + + private async Task LoadEntity(string name) + { + var path = GetEntityPath(name); + var entity = new EntityModel(name, path, await _context.ReadFile(path)); + entity.Initialize(); + return entity; + } + + private string GetEntityPath(string name) => $"orm/entities/{name.Underscore().ToLower()}.py"; +} \ No newline at end of file diff --git a/MycroForge.CLI/CodeGen/PythonSourceModifier.cs b/MycroForge.CLI/CodeGen/PythonSourceModifier.cs index c469222..1f84b9b 100644 --- a/MycroForge.CLI/CodeGen/PythonSourceModifier.cs +++ b/MycroForge.CLI/CodeGen/PythonSourceModifier.cs @@ -5,9 +5,9 @@ namespace MycroForge.CLI.CodeGen; public abstract class PythonSourceModifier : PythonParserBaseVisitor { - private CommonTokenStream Stream { get; } - private PythonParser Parser { get; } - private TokenStreamRewriter Rewriter { get; } + protected CommonTokenStream Stream { get; } + protected PythonParser Parser { get; } + protected TokenStreamRewriter Rewriter { get; } protected PythonSourceModifier(string source) { @@ -18,7 +18,7 @@ public abstract class PythonSourceModifier : PythonParserBaseVisitor Rewriter = new TokenStreamRewriter(Stream); } - public string Rewrite() + public virtual string Rewrite() { var tree = Parser.file_input(); Visit(tree); diff --git a/MycroForge.CLI/Commands/MycroForge.Add.Api.cs b/MycroForge.CLI/Commands/MycroForge.Add.Api.cs deleted file mode 100644 index 3c69f2f..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Add.Api.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; -using MycroForge.CLI.Features; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Add - { - public class Api : Command, ISubCommandOf - { - public Api(ProjectContext context, IEnumerable features) : - base("api", "Add FastAPI to your project") - { - var feature = features.First(f => f.Name == Features.Api.FeatureName); - this.SetHandler(async () => await feature.ExecuteAsync(context)); - } - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Add.Orm.cs b/MycroForge.CLI/Commands/MycroForge.Add.Orm.cs deleted file mode 100644 index 9847862..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Add.Orm.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; -using MycroForge.CLI.Features; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Add - { - public class Orm : Command, ISubCommandOf - { - public Orm(ProjectContext context, IEnumerable features) : - base("orm", "Add SQLAlchemy to your project") - { - var feature = features.First(f => f.Name == Features.Orm.FeatureName); - this.SetHandler(async () => await feature.ExecuteAsync(context)); - } - - public class Generate - { - - } - } - } -} diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs new file mode 100644 index 0000000..1fa2b42 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Api.Generate.Router.cs @@ -0,0 +1,56 @@ +using System.CommandLine; +using Humanizer; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Api + { + public partial class Generate + { + public class Router : Command, ISubCommandOf + { + private static readonly string[] Template = + [ + "from fastapi import APIRouter", + "from fastapi.responses import JSONResponse", + "from fastapi.encoders import jsonable_encoder", + "", + "router = APIRouter()", + "", + "@router.get(\"/{name}\")", + "async def index(name: str):", + "\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))" + ]; + + private static readonly Argument NameArgument = + new(name: "name", description: "The name of the api router"); + + private readonly ProjectContext _context; + + public Router(ProjectContext context) : base("router", "Generate an api router") + { + _context = context; + AddAlias("r"); + AddArgument(NameArgument); + this.SetHandler(ExecuteAsync, NameArgument); + } + + private async Task ExecuteAsync(string name) + { + var moduleName = name.Underscore(); + await _context.CreateFile($"api/routers/{moduleName}.py", Template); + + var main = await _context.ReadFile("main.py"); + main += string.Join('\n', + $"\nfrom api.routers import {moduleName}", + $"app.include_router(prefix=\"/{name.Kebaberize()}\", router={moduleName}.router)\n" + ); + await _context.WriteFile("main.py", main); + } + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Generate.cs b/MycroForge.CLI/Commands/MycroForge.Api.Generate.cs new file mode 100644 index 0000000..86dd28d --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Api.Generate.cs @@ -0,0 +1,21 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Api + { + public partial class Generate : Command, ISubCommandOf + { + public Generate(IEnumerable> subCommands) : + base("generate", "Generate an API item") + { + AddAlias("g"); + foreach (var subCommandOf in subCommands.Cast()) + AddCommand(subCommandOf); + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Api.Run.cs b/MycroForge.CLI/Commands/MycroForge.Api.Run.cs new file mode 100644 index 0000000..9ebb578 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Api.Run.cs @@ -0,0 +1,26 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Api + { + public class Run : Command, ISubCommandOf + { + public Run() : base("run", "Run your app") + { + this.SetHandler(ExecuteAsync); + } + + private async Task ExecuteAsync() + { + await Bash.ExecuteAsync([ + "source .venv/bin/activate", + "uvicorn main:app --reload" + ]); + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Add.cs b/MycroForge.CLI/Commands/MycroForge.Api.cs similarity index 51% rename from MycroForge.CLI/Commands/MycroForge.Add.cs rename to MycroForge.CLI/Commands/MycroForge.Api.cs index b45d690..8a14aef 100644 --- a/MycroForge.CLI/Commands/MycroForge.Add.cs +++ b/MycroForge.CLI/Commands/MycroForge.Api.cs @@ -1,14 +1,14 @@ -using System.CommandLine; +using System.CommandLine; using MycroForge.CLI.Commands.Interfaces; namespace MycroForge.CLI.Commands; public partial class MycroForge { - public new partial class Add : Command, ISubCommandOf + public partial class Api : Command, ISubCommandOf { - public Add(IEnumerable> subCommands) : - base("add", "Add a predefined feature to your project") + public Api(IEnumerable> subCommands) : + base("api", "API related commands") { foreach (var subCommandOf in subCommands) AddCommand((subCommandOf as Command)!); diff --git a/MycroForge.CLI/Commands/MycroForge.Entity.Link.Many.cs b/MycroForge.CLI/Commands/MycroForge.Entity.Link.Many.cs deleted file mode 100644 index 304a0bc..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Entity.Link.Many.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Entity - { - public partial class Link - { - public class Many : Command, ISubCommandOf - { - private static readonly Argument NameArgument = - new(name: "primary", description: "The left side of the relation"); - - private static readonly Option ToOneOption = - new(name: "--to-one", description: "The right side of the relation"); - - private static readonly Option ToManyOption = - new(name: "--to-many", description: "The right side of the relation"); - - public Many() : base("many", "Define a n:m relation") - { - AddArgument(NameArgument); - AddOption(ToOneOption); - AddOption(ToManyOption); - this.SetHandler(ExecuteAsync); - } - - private async Task ExecuteAsync() - { - } - } - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Entity.Link.One.cs b/MycroForge.CLI/Commands/MycroForge.Entity.Link.One.cs deleted file mode 100644 index 75820d3..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Entity.Link.One.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Entity - { - public partial class Link - { - public class One : Command, ISubCommandOf - { - private static readonly Argument NameArgument = - new(name: "primary", description: "The left side of the relation"); - - private static readonly Option ToOneOption = - new(name: "--to-one", description: "The right side of the relation"); - - private static readonly Option ToManyOption = - new(name: "--to-many", description: "The right side of the relation"); - - - public One() : base("one", "Define a 1:n relation") - { - AddArgument(NameArgument); - AddOption(ToOneOption); - AddOption(ToManyOption); - this.SetHandler(ExecuteAsync); - } - - private async Task ExecuteAsync() - { - } - } - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Entity.cs b/MycroForge.CLI/Commands/MycroForge.Entity.cs deleted file mode 100644 index 158f23c..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Entity.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Entity : Command, ISubCommandOf - { - public Entity(IEnumerable> commands) : - base("entity", "Manage the entities in your project") - { - foreach (var command in commands.Cast()) - AddCommand(command); - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Entity.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Entity.cs deleted file mode 100644 index f6ec7af..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Generate.Entity.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.CommandLine; -using Humanizer; -using MycroForge.CLI.CodeGen; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Generate - { - // ReSharper disable once MemberHidesStaticFromOuterClass - public class Entity : Command, ISubCommandOf - { - private static readonly string[] Template = - [ - "from sqlalchemy import String", - "from sqlalchemy.orm import Mapped, mapped_column", - "from orm.entities.entity_base import EntityBase", - "", - "class %class_name%(EntityBase):", - "\t__tablename__ = \"%table_name%\"", - "\tid: Mapped[int] = mapped_column(primary_key=True)", - "\tvalue: Mapped[str] = mapped_column(String(255))", - "", - "\tdef __repr__(self) -> str:", - "\t\treturn f\"%class_name%(id={self.id!r}, value={self.value!r})\"" - ]; - - private static readonly Argument NameArgument = - new(name: "name", description: "The name of the orm entity"); - - private readonly ProjectContext _context; - - public Entity(ProjectContext context) : base("entity", "Generate and orm entity") - { - _context = context; - AddAlias("e"); - AddArgument(NameArgument); - this.SetHandler(ExecuteAsync, NameArgument); - } - - private async Task ExecuteAsync(string name) - { - var className = name.Underscore().Pascalize(); - var moduleName = name.Underscore(); - var code = string.Join('\n', Template); - - code = code.Replace("%class_name%", className); - code = code.Replace("%table_name%", name.ToLower().Underscore()); - await _context.CreateFile($"orm/entities/{moduleName}.py", code); - - var env = await _context.ReadFile("orm/env.py"); - env = new OrmEnvUpdater(env, moduleName, className).Rewrite(); - await _context.WriteFile("orm/env.py", env); - } - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Migration.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Migration.cs deleted file mode 100644 index 7110122..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Generate.Migration.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Generate - { - public class Migration : Command, ISubCommandOf - { - private static readonly Argument NameArgument = - new(name: "name", description: "The name of the migration"); - - - public Migration() : base("migration", "Generate a migration") - { - AddAlias("m"); - AddArgument(NameArgument); - this.SetHandler(ExecuteAsync, NameArgument); - } - - private async Task ExecuteAsync(string name) - { - await Bash.ExecuteAsync( - "source .venv/bin/activate", - $"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")" - ); - } - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.Router.cs b/MycroForge.CLI/Commands/MycroForge.Generate.Router.cs deleted file mode 100644 index f2eee2d..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Generate.Router.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.CommandLine; -using Humanizer; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Generate - { - public class Router : Command, ISubCommandOf - { - private static readonly string[] Template = - [ - "from fastapi import APIRouter", - "from fastapi.responses import JSONResponse", - "from fastapi.encoders import jsonable_encoder", - "", - "router = APIRouter()", - "", - "@router.get(\"/{name}\")", - "async def index(name: str):", - "\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))" - ]; - - private static readonly Argument NameArgument = - new(name: "name", description: "The name of the api router"); - - private readonly ProjectContext _context; - - public Router(ProjectContext context) : base("router", "Generate an api router") - { - _context = context; - AddAlias("r"); - AddArgument(NameArgument); - this.SetHandler(ExecuteAsync, NameArgument); - } - - private async Task ExecuteAsync(string name) - { - var moduleName = name.Underscore(); - await _context.CreateFile($"api/routers/{moduleName}.py", Template); - - var main = await _context.ReadFile("main.py"); - main += string.Join('\n', - $"\nfrom api.routers import {moduleName}", - $"app.include_router(prefix=\"/{name.Kebaberize()}\", router={moduleName}.router)\n" - ); - await _context.WriteFile("main.py", main); - } - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Generate.cs b/MycroForge.CLI/Commands/MycroForge.Generate.cs deleted file mode 100644 index 72d226b..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Generate.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Generate : Command, ISubCommandOf - { - public Generate(IEnumerable> subCommands) : - base("generate", "Generate a project item") - { - AddAlias("g"); - foreach (var subCommandOf in subCommands) - AddCommand((subCommandOf as Command)!); - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Init.cs b/MycroForge.CLI/Commands/MycroForge.Init.cs index ac878fa..bc5a2c2 100644 --- a/MycroForge.CLI/Commands/MycroForge.Init.cs +++ b/MycroForge.CLI/Commands/MycroForge.Init.cs @@ -1,5 +1,4 @@ using System.CommandLine; -using Microsoft.Extensions.DependencyInjection; using MycroForge.CLI.Commands.Interfaces; using MycroForge.CLI.Features; @@ -9,127 +8,60 @@ public partial class MycroForge { public class Init : Command, ISubCommandOf { - #region GitIgnore - - private static readonly string[] GitIgnore = + private static readonly string[] DefaultFeatures = [ - "# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions", - "*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/", - ".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/", - ".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller", - "# Usually these files are written by a python script from a template", - "# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec", - "# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports", - "htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml", - "*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot", - "# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:", - "instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/", - "# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython", - "profile_default/", "ipython_config.py", "# pyenv", - "# For a library or package, you might want to ignore these files since the code is", - "# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv", - "# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.", - "# However, in case of collaboration, if having platform-specific dependencies or dependencies", - "# having no cross-platform support, pipenv may install dependencies that don't work, or not", - "# install all needed dependencies.", "#Pipfile.lock", "# poetry", - "# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.", - "# This is especially recommended for binary packages to ensure reproducibility, and is more", - "# commonly ignored for libraries.", - "# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control", - "#poetry.lock", "# pdm", - "# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.", - "#pdm.lock", - "# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it", - "# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml", - "# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/", - "# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py", - "# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/", - "# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject", - "# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json", - "# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols", - "cython_debug/", "# PyCharm", - "# JetBrains specific template is maintained in a separate JetBrains.gitignore that can", - "# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore", - "# and can be added to the global gitignore or merged into this file. For a more nuclear", - "# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/" + Features.Git.FeatureName, + Features.Api.FeatureName, + Features.Orm.FeatureName ]; - #endregion - private static readonly Argument NameArgument = new(name: "name", description: "The name of your project"); - private static readonly Option EntryPoint = - new(name: "--entrypoint", description: "The name of the entrypoint file"); - - private static readonly Option BranchOption = - new(name: "--branch", description: "The name of the initial git branch"); - - private static readonly Option> FeaturesOption = - new(name: "--features", description: "The features to include") - { - AllowMultipleArgumentsPerToken = true - }; - - private readonly IServiceProvider _services; + private static readonly Option> WithoutOption = + new Option>(name: "--without", description: "Features to exclude") + .FromAmong(DefaultFeatures); + private readonly ProjectContext _context; private readonly List _features; - public Init(IServiceProvider services) : base("init", "Initialize a new project") + public Init(ProjectContext context, IEnumerable features) : + base("init", "Initialize a new project") { + _context = context; + _features = features.ToList(); + AddArgument(NameArgument); - AddOption(EntryPoint); - AddOption(BranchOption); - AddOption(FeaturesOption); - this.SetHandler(ExecuteAsync, NameArgument, EntryPoint, BranchOption, FeaturesOption); - - _services = services; - _features = _services.GetServices().ToList(); + AddOption(WithoutOption); + this.SetHandler(ExecuteAsync, NameArgument, WithoutOption); } - private async Task ExecuteAsync(string name, string entrypoint, string branch, IEnumerable features) + private async Task ExecuteAsync(string name, IEnumerable without) { - var featuresList = features.ToList(); - Validate(featuresList); - await Initialize(name, entrypoint, branch, featuresList); - } - - private void Validate(List features) - { - foreach (var feature in features) + // Validate excluded features + var withoutList = without.ToList(); + foreach (var feature in withoutList) if (_features.All(f => f.Name != feature)) - throw new Exception($"Feature {feature} was not found."); - } + throw new Exception($"Feature {feature} does not exist."); - private async Task Initialize(string name, string entrypoint, string branch, List features) - { // Create the project directory and change the directory for the ProjectContext var projectRoot = await CreateDirectory(name); - var ctx = _services.GetRequiredService(); - ctx.ChangeDirectory(projectRoot); + _context.ChangeDirectory(projectRoot); // Create the config file and initialize the config - await ctx.CreateFile("m4g.json", "{}"); - await ctx.LoadConfig(force: true); + await _context.CreateFile("m4g.json", "{}"); + await _context.LoadConfig(force: true); // Create the entrypoint file - entrypoint = string.IsNullOrEmpty(entrypoint) ? "main.py" : entrypoint; - await ctx.CreateFile(entrypoint, string.Empty); - ctx.Config.Entrypoint = entrypoint; - - // Create the default .gitignore - await ctx.CreateFile(".gitignore", GitIgnore); + await _context.CreateFile("main.py"); // Create the venv await Bash.ExecuteAsync($"python3 -m venv {Path.Combine(projectRoot, ".venv")}"); - // Initialize git - var _branch = string.IsNullOrEmpty(branch) ? "main" : branch; - await Bash.ExecuteAsync($"git -c init.defaultBranch={_branch} init {projectRoot}"); - - // Initialize features - if (features.Count > 0) - await InitializeFeatures(ctx, features); + // Initialize default features + foreach (var feature in _features.Where(f => DefaultFeatures.Contains(f.Name))) + if (!withoutList.Contains(feature.Name)) + await feature.ExecuteAsync(_context); Console.WriteLine($"Directory {projectRoot} was successfully initialized"); } @@ -149,11 +81,5 @@ public partial class MycroForge return directory; } - - private async Task InitializeFeatures(ProjectContext projectCtx, List features) - { - foreach (var feature in features) - await _features.First(p => p.Name == feature).ExecuteAsync(projectCtx); - } } } \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Migrations.cs b/MycroForge.CLI/Commands/MycroForge.Migrations.cs deleted file mode 100644 index a80f474..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Migrations.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public partial class Migrations : Command, ISubCommandOf - { - public Migrations(IEnumerable> subCommands) : - base("migrations", "Manage your migrations") - { - AddAlias("m"); - foreach (var subCommandOf in subCommands) - AddCommand((subCommandOf as Command)!); - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs new file mode 100644 index 0000000..7574a6f --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Entity.cs @@ -0,0 +1,61 @@ +using System.CommandLine; +using Humanizer; +using MycroForge.CLI.CodeGen; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Orm + { + public partial class Generate + { + public class Entity : Command, ISubCommandOf + { + private static readonly string[] Template = + [ + "from sqlalchemy import String", + "from sqlalchemy.orm import Mapped, mapped_column", + "from orm.entities.entity_base import EntityBase", + "", + "class %class_name%(EntityBase):", + "\t__tablename__ = \"%table_name%\"", + "\tid: Mapped[int] = mapped_column(primary_key=True)", + "\tvalue: Mapped[str] = mapped_column(String(255))", + "", + "\tdef __repr__(self) -> str:", + "\t\treturn f\"%class_name%(id={self.id!r}, value={self.value!r})\"" + ]; + + private static readonly Argument NameArgument = + new(name: "name", description: "The name of the orm entity"); + + private readonly ProjectContext _context; + + public Entity(ProjectContext context) : base("entity", "Generate and orm entity") + { + _context = context; + AddAlias("e"); + AddArgument(NameArgument); + this.SetHandler(ExecuteAsync, NameArgument); + } + + private async Task ExecuteAsync(string name) + { + var className = name.Underscore().Pascalize(); + var moduleName = name.Underscore(); + var code = string.Join('\n', Template); + + code = code.Replace("%class_name%", className); + code = code.Replace("%table_name%", name.Underscore().ToLower()); + await _context.CreateFile($"orm/entities/{moduleName}.py", code); + + var env = await _context.ReadFile("orm/env.py"); + env = new OrmEnvUpdater(env, moduleName, className).Rewrite(); + await _context.WriteFile("orm/env.py", env); + } + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs new file mode 100644 index 0000000..a8bb919 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.Migration.cs @@ -0,0 +1,34 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Orm + { + public partial class Generate + { + public class Migration : Command, ISubCommandOf + { + private static readonly Argument NameArgument = + new(name: "name", description: "The name of the migration"); + + public Migration() : base("migration", "Generate a migration") + { + AddAlias("m"); + AddArgument(NameArgument); + this.SetHandler(ExecuteAsync, NameArgument); + } + + private async Task ExecuteAsync(string name) + { + await Bash.ExecuteAsync( + "source .venv/bin/activate", + $"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")" + ); + } + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Generate.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.cs new file mode 100644 index 0000000..de6ed4b --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Generate.cs @@ -0,0 +1,22 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Orm + { + public partial class Generate : Command, ISubCommandOf + { + public Generate(IEnumerable> subCommands) : + base("generate", "Generate an ORM item") + { + AddAlias("g"); + foreach (var subCommandOf in subCommands.Cast()) + AddCommand(subCommandOf); + } + + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Link.Many.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Link.Many.cs new file mode 100644 index 0000000..337bc39 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Link.Many.cs @@ -0,0 +1,62 @@ +using System.CommandLine; +using MycroForge.CLI.CodeGen; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Orm + { + public partial class Link + { + public class Many : Command, ISubCommandOf + { + private readonly ProjectContext _context; + + private static readonly Argument LeftArgument = + new(name: "entity", description: "The left side of the relation"); + + private static readonly Option ToOneOption = + new(name: "--to-one", description: "The right side of the relation"); + + private static readonly Option ToManyOption = + new(name: "--to-many", description: "The right side of the relation"); + + public Many(ProjectContext context) : base("many", "Define a n:m relation") + { + _context = context; + AddArgument(LeftArgument); + AddOption(ToOneOption); + AddOption(ToManyOption); + this.SetHandler(ExecuteAsync, LeftArgument, ToOneOption, ToManyOption); + } + + + private async Task ExecuteAsync(string left, string? toOneOption, string? toManyOption) + { + if (toOneOption is not null && toManyOption is not null) + throw new Exception("Cannot set both --to-one and --to-many option."); + + if (toOneOption is not null) + await ManyToOne(left, toOneOption); + + else if (toManyOption is not null) + await ManyToMany(left, toManyOption); + + else throw new Exception("Set --to-one or --to-many option."); + } + + private async Task ManyToOne(string left, string toOneOption) + { + await new EntityLinker(_context, left, toOneOption).ManyToOne(); + } + + private async Task ManyToMany(string left, string toManyOption) + { + await new EntityLinker(_context, left, toManyOption).ManyToMany(); + } + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.Link.One.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Link.One.cs new file mode 100644 index 0000000..04e871f --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Link.One.cs @@ -0,0 +1,62 @@ +using System.CommandLine; +using MycroForge.CLI.CodeGen; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Orm + { + public partial class Link + { + public class One : Command, ISubCommandOf + { + private readonly ProjectContext _context; + + private static readonly Argument LeftArgument = + new(name: "entity", description: "The left side of the relation"); + + private static readonly Option ToOneOption = + new(name: "--to-one", description: "The right side of the relation"); + + private static readonly Option ToManyOption = + new(name: "--to-many", description: "The right side of the relation"); + + + public One(ProjectContext context) : base("one", "Define a 1:n relation") + { + _context = context; + AddArgument(LeftArgument); + AddOption(ToOneOption); + AddOption(ToManyOption); + this.SetHandler(ExecuteAsync, LeftArgument, ToOneOption, ToManyOption); + } + + private async Task ExecuteAsync(string left, string? toOneOption, string? toManyOption) + { + if (toOneOption is not null && toManyOption is not null) + throw new Exception("Cannot set both --to-one and --to-many option."); + + if (toOneOption is not null) + await OneToOne(left, toOneOption); + + else if (toManyOption is not null) + await OneToMany(left, toManyOption); + + else throw new Exception("Set --to-one or --to-many option."); + } + + private async Task OneToOne(string left, string toOneOption) + { + await new EntityLinker(_context, left, toOneOption).OneToOne(); + } + + private async Task OneToMany(string left, string toManyOption) + { + await new EntityLinker(_context, left, toManyOption).OneToMany(); + } + } + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Entity.Link.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Link.cs similarity index 55% rename from MycroForge.CLI/Commands/MycroForge.Entity.Link.cs rename to MycroForge.CLI/Commands/MycroForge.Orm.Link.cs index c12d2db..dce0eb4 100644 --- a/MycroForge.CLI/Commands/MycroForge.Entity.Link.cs +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Link.cs @@ -1,19 +1,19 @@ -using System.CommandLine; +using System.CommandLine; using MycroForge.CLI.Commands.Interfaces; namespace MycroForge.CLI.Commands; public partial class MycroForge { - public partial class Entity + public partial class Orm { - public partial class Link : Command, ISubCommandOf + public partial class Link : Command, ISubCommandOf { public Link(IEnumerable> commands) : base("link", "Define relationships between entities") { - foreach (var command in commands) - AddCommand((command as Command)!); + foreach (var command in commands.Cast()) + AddCommand(command); } } } diff --git a/MycroForge.CLI/Commands/MycroForge.Migrations.Apply.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Migrate.cs similarity index 62% rename from MycroForge.CLI/Commands/MycroForge.Migrations.Apply.cs rename to MycroForge.CLI/Commands/MycroForge.Orm.Migrate.cs index 6be1337..9f2fc10 100644 --- a/MycroForge.CLI/Commands/MycroForge.Migrations.Apply.cs +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Migrate.cs @@ -1,17 +1,16 @@ -using System.CommandLine; +using System.CommandLine; using MycroForge.CLI.Commands.Interfaces; namespace MycroForge.CLI.Commands; public partial class MycroForge { - public partial class Migrations + public partial class Orm { - public class Apply : Command, ISubCommandOf + public class Migrate : Command, ISubCommandOf { - public Apply() : base("apply", "Apply migrations to the database") + public Migrate() : base("migrate", "Apply migrations to the database") { - AddAlias("a"); this.SetHandler(ExecuteAsync); } diff --git a/MycroForge.CLI/Commands/MycroForge.Migrations.Rollback.cs b/MycroForge.CLI/Commands/MycroForge.Orm.Rollback.cs similarity index 73% rename from MycroForge.CLI/Commands/MycroForge.Migrations.Rollback.cs rename to MycroForge.CLI/Commands/MycroForge.Orm.Rollback.cs index bcaebaa..e17a0de 100644 --- a/MycroForge.CLI/Commands/MycroForge.Migrations.Rollback.cs +++ b/MycroForge.CLI/Commands/MycroForge.Orm.Rollback.cs @@ -1,17 +1,16 @@ -using System.CommandLine; +using System.CommandLine; using MycroForge.CLI.Commands.Interfaces; namespace MycroForge.CLI.Commands; public partial class MycroForge { - public partial class Migrations + public partial class Orm { - public class Rollback : Command, ISubCommandOf + public class Rollback : Command, ISubCommandOf { public Rollback() : base("rollback", "Rollback the last migration") { - AddAlias("r"); this.SetHandler(ExecuteAsync); } @@ -24,4 +23,4 @@ public partial class MycroForge } } } -} +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Orm.cs b/MycroForge.CLI/Commands/MycroForge.Orm.cs new file mode 100644 index 0000000..8a00e45 --- /dev/null +++ b/MycroForge.CLI/Commands/MycroForge.Orm.cs @@ -0,0 +1,17 @@ +using System.CommandLine; +using MycroForge.CLI.Commands.Interfaces; + +namespace MycroForge.CLI.Commands; + +public partial class MycroForge +{ + public partial class Orm : Command, ISubCommandOf + { + public Orm(IEnumerable> subCommands) + : base("orm", "ORM related commands") + { + foreach (var subCommandOf in subCommands.Cast()) + AddCommand(subCommandOf); + } + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Commands/MycroForge.Run.cs b/MycroForge.CLI/Commands/MycroForge.Run.cs deleted file mode 100644 index 04b918f..0000000 --- a/MycroForge.CLI/Commands/MycroForge.Run.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.CommandLine; -using MycroForge.CLI.Commands.Interfaces; - -namespace MycroForge.CLI.Commands; - -public partial class MycroForge -{ - public class Run : Command, ISubCommandOf - { - public Run() : base("run", "Run your app") - { - this.SetHandler(ExecuteAsync); - } - - private async Task ExecuteAsync() - { - await Bash.ExecuteAsync([ - "source .venv/bin/activate", - "uvicorn main:app --reload" - ]); - } - } -} \ No newline at end of file diff --git a/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs b/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs index a48c7ce..ae6b491 100644 --- a/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs +++ b/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs @@ -15,6 +15,7 @@ public static class ServiceCollectionExtensions Args = args.Length == 0 ? ["--help"] : args }); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -26,31 +27,26 @@ public static class ServiceCollectionExtensions // Register "m4g" services.AddScoped(); services.AddScoped, Commands.MycroForge.Init>(); - services.AddScoped, Commands.MycroForge.Run>(); services.AddScoped, Commands.MycroForge.Install>(); services.AddScoped, Commands.MycroForge.Uninstall>(); - // Register "m4g add" - services.AddScoped, Commands.MycroForge.Add>(); - services.AddScoped, Commands.MycroForge.Add.Api>(); - services.AddScoped, Commands.MycroForge.Add.Orm>(); + // Register "m4g api" + services.AddScoped, Commands.MycroForge.Api>(); + services.AddScoped, Commands.MycroForge.Api.Run>(); + services.AddScoped, Commands.MycroForge.Api.Generate>(); + services.AddScoped, Commands.MycroForge.Api.Generate.Router>(); - // Register "m4g generate" - services.AddScoped, Commands.MycroForge.Generate>(); - services.AddScoped, Commands.MycroForge.Generate.Entity>(); - services.AddScoped, Commands.MycroForge.Generate.Router>(); - services.AddScoped, Commands.MycroForge.Generate.Migration>(); - - // Register "m4g entity" - services.AddScoped, Commands.MycroForge.Entity>(); - services.AddScoped, Commands.MycroForge.Entity.Link>(); - services.AddScoped, Commands.MycroForge.Entity.Link.One>(); - services.AddScoped, Commands.MycroForge.Entity.Link.Many>(); - - // Register "m4g migrations" - services.AddScoped, Commands.MycroForge.Migrations>(); - services.AddScoped, Commands.MycroForge.Migrations.Apply>(); - services.AddScoped, Commands.MycroForge.Migrations.Rollback>(); + // Register "m4g orm" + services.AddScoped, Commands.MycroForge.Orm>(); + services.AddScoped, Commands.MycroForge.Orm.Migrate>(); + services.AddScoped, Commands.MycroForge.Orm.Rollback>(); + services.AddScoped, Commands.MycroForge.Orm.Generate>(); + services.AddScoped, Commands.MycroForge.Orm.Generate.Entity>(); + services + .AddScoped, Commands.MycroForge.Orm.Generate.Migration>(); + services.AddScoped, Commands.MycroForge.Orm.Link>(); + services.AddScoped, Commands.MycroForge.Orm.Link.One>(); + services.AddScoped, Commands.MycroForge.Orm.Link.Many>(); return services; } diff --git a/MycroForge.CLI/Features/Api.cs b/MycroForge.CLI/Features/Api.cs index 898f0be..93068f6 100644 --- a/MycroForge.CLI/Features/Api.cs +++ b/MycroForge.CLI/Features/Api.cs @@ -40,19 +40,6 @@ public sealed class Api : IFeature return; } - Console.WriteLine(string.Join("\n", [ - $"Adding feature {FeatureName}", - "Requirements:", - " - fastapi", - " - uvicorn[standard]", - ])); - - await Bash.ExecuteAsync( - "source .venv/bin/activate", - "python3 -m pip install fastapi uvicorn[standard]", - "python3 -m pip freeze > requirements.txt" - ); - await context.CreateFile("api/routers/hello.py", HelloRouter); var main = await context.ReadFile("main.py"); diff --git a/MycroForge.CLI/Features/Git.cs b/MycroForge.CLI/Features/Git.cs new file mode 100644 index 0000000..8a16dcf --- /dev/null +++ b/MycroForge.CLI/Features/Git.cs @@ -0,0 +1,60 @@ +namespace MycroForge.CLI.Features; + +public class Git : IFeature +{ + #region GitIgnore + + private static readonly string[] GitIgnore = + [ + "# Byte-compiled / optimized / DLL files", "__pycache__/", "*.py[cod]", "*$py.class", "# C extensions", + "*.so", "# Distribution / packaging", ".Python", "build/", "develop-eggs/", "dist/", "downloads/", "eggs/", + ".eggs/", "lib/", "lib64/", "parts/", "sdist/", "var/", "wheels/", "share/python-wheels/", "*.egg-info/", + ".installed.cfg", "*.egg", "MANIFEST", "# PyInstaller", + "# Usually these files are written by a python script from a template", + "# before PyInstaller builds the exe, so as to inject date/other infos into it.", "*.manifest", "*.spec", + "# Installer logs", "pip-log.txt", "pip-delete-this-directory.txt", "# Unit test / coverage reports", + "htmlcov/", ".tox/", ".nox/", ".coverage", ".coverage.*", ".cache", "nosetests.xml", "coverage.xml", + "*.cover", "*.py,cover", ".hypothesis/", ".pytest_cache/", "cover/", "# Translations", "*.mo", "*.pot", + "# Django stuff:", "*.log", "local_settings.py", "db.sqlite3", "db.sqlite3-journal", "# Flask stuff:", + "instance/", ".webassets-cache", "# Scrapy stuff:", ".scrapy", "# Sphinx documentation", "docs/_build/", + "# PyBuilder", ".pybuilder/", "target/", "# Jupyter Notebook", ".ipynb_checkpoints", "# IPython", + "profile_default/", "ipython_config.py", "# pyenv", + "# For a library or package, you might want to ignore these files since the code is", + "# intended to run in multiple environments; otherwise, check them in:", "# .python-version", "# pipenv", + "# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.", + "# However, in case of collaboration, if having platform-specific dependencies or dependencies", + "# having no cross-platform support, pipenv may install dependencies that don't work, or not", + "# install all needed dependencies.", "#Pipfile.lock", "# poetry", + "# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.", + "# This is especially recommended for binary packages to ensure reproducibility, and is more", + "# commonly ignored for libraries.", + "# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control", + "#poetry.lock", "# pdm", + "# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.", + "#pdm.lock", + "# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it", + "# in version control.", "# https://pdm.fming.dev/#use-with-ide", ".pdm.toml", + "# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm", "__pypackages__/", + "# Celery stuff", "celerybeat-schedule", "celerybeat.pid", "# SageMath parsed files", "*.sage.py", + "# Environments", ".env", ".venv", "env/", "venv/", "ENV/", "env.bak/", "venv.bak/", + "# Spyder project settings", ".spyderproject", ".spyproject", "# Rope project settings", ".ropeproject", + "# mkdocs documentation", "/site", "# mypy", ".mypy_cache/", ".dmypy.json", "dmypy.json", + "# Pyre type checker", ".pyre/", "# pytype static type analyzer", ".pytype/", "# Cython debug symbols", + "cython_debug/", "# PyCharm", + "# JetBrains specific template is maintained in a separate JetBrains.gitignore that can", + "# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore", + "# and can be added to the global gitignore or merged into this file. For a more nuclear", + "# option (not recommended) you can uncomment the following to ignore the entire idea folder.", "#.idea/" + ]; + + #endregion + + public const string FeatureName = "git"; + public string Name => FeatureName; + + public async Task ExecuteAsync(ProjectContext context) + { + await context.CreateFile(".gitignore", GitIgnore); + await Bash.ExecuteAsync($"git -c init.defaultBranch=main init {context.RootDirectory}"); + } +} \ No newline at end of file diff --git a/MycroForge.CLI/Features/Orm.cs b/MycroForge.CLI/Features/Orm.cs index 29e2b29..463ef1d 100644 --- a/MycroForge.CLI/Features/Orm.cs +++ b/MycroForge.CLI/Features/Orm.cs @@ -64,14 +64,6 @@ public sealed class Orm : IFeature return; } - Console.WriteLine(string.Join("\n", [ - $"Adding feature {FeatureName}", - "Requirements:", - " - asyncmy", - " - sqlalchemy", - " - alembic", - ])); - await Bash.ExecuteAsync( "source .venv/bin/activate", "python3 -m pip install asyncmy sqlalchemy alembic", diff --git a/MycroForge.CLI/Program.cs b/MycroForge.CLI/Program.cs index 6adf526..b38a6b4 100644 --- a/MycroForge.CLI/Program.cs +++ b/MycroForge.CLI/Program.cs @@ -1,32 +1,53 @@ -// using System.CommandLine; -// using MycroForge.CLI; -// using MycroForge.CLI.Exceptions; -// using MycroForge.CLI.Extensions; -// using Microsoft.Extensions.DependencyInjection; -// using Microsoft.Extensions.Hosting; +using System.CommandLine; +using MycroForge.CLI; +using MycroForge.CLI.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +using var host = Host + .CreateDefaultBuilder() + .ConfigureServices((_, services) => + { + services + .AddServices(args) + .AddCommands(); + }) + .Build(); + +try +{ + var ctx = host.Services.GetRequiredService(); + await ctx.LoadConfig(); + await host.Services.GetRequiredService().InvokeAsync(args); + await ctx.SaveConfig(); +} +catch(Exception e) +{ + Console.WriteLine(e.Message); +} + + +// using MycroForge.CLI.CodeGen; +// using MycroForge.Parsing; // -// using var host = Host -// .CreateDefaultBuilder() -// .ConfigureServices((_, services) => +// var src = new Tester().Rewrite(); +// Console.WriteLine(src); +// +// class Tester : PythonSourceModifier +// { +// public Tester() : base(File.ReadAllText("scripts/user.py")) // { -// services -// .AddServices(args) -// .AddCommands(); -// }) -// .Build(); +// +// } // -// try -// { -// var ctx = host.Services.GetRequiredService(); -// await ctx.LoadConfig(); -// await host.Services.GetRequiredService().InvokeAsync(args); -// await ctx.SaveConfig(); -// } -// catch(Exception e) -// { -// Console.WriteLine(e.Message); +// public override object? VisitAssignment(PythonParser.AssignmentContext context) +// { +// Console.WriteLine(GetOriginalText(context)); +// return base.VisitAssignment(context); +// } // } -using MycroForge.CLI.CodeGen; -var src = new EntityLinker(await File.ReadAllTextAsync("scripts/user.py")).Rewrite(); + +// using MycroForge.CLI.CodeGen; +// var src = new EntityModifier(await File.ReadAllTextAsync("scripts/user.py")).Rewrite(); // Console.WriteLine(src); \ No newline at end of file diff --git a/MycroForge.CLI/ProjectConfig.cs b/MycroForge.CLI/ProjectConfig.cs index b3ae6ee..24ca395 100644 --- a/MycroForge.CLI/ProjectConfig.cs +++ b/MycroForge.CLI/ProjectConfig.cs @@ -2,6 +2,5 @@ public class ProjectConfig { - public string Entrypoint { get; set; } = string.Empty; public List Features { get; set; } = new(); } \ No newline at end of file