diff --git a/.gitignore b/.gitignore index 2c69ae7b..c7df4fee 100644 --- a/.gitignore +++ b/.gitignore @@ -375,3 +375,6 @@ MigrationBackup/ # Fody - auto-generated XML schema FodyWeavers.xsd src/EdgeDB.ExampleApp/dump.db + +# EdgeDB.Net CLI watcher info file +edgeql.dotnet.watcher.process \ No newline at end of file diff --git a/EdgeDB.Net.sln b/EdgeDB.Net.sln index 5e490f2b..459228e6 100644 --- a/EdgeDB.Net.sln +++ b/EdgeDB.Net.sln @@ -21,8 +21,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{67ED9EF0 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.QueryBuilder.OperatorGenerator", "tools\EdgeDB.QueryBuilder.OperatorGenerator\EdgeDB.QueryBuilder.OperatorGenerator.csproj", "{1557B745-EB4F-449A-9BE7-180C8990AD47}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.DotnetTool", "tools\EdgeDB.DotnetTool\EdgeDB.DotnetTool.csproj", "{74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Tests.Integration", "tests\EdgeDB.Tests.Integration\EdgeDB.Tests.Integration.csproj", "{C189294A-4990-4A06-B120-A0AF03A798C6}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Tests.Benchmarks", "tests\EdgeDB.Tests.Benchmarks\EdgeDB.Tests.Benchmarks.csproj", "{5FFD1E88-614D-409B-8420-F9571AC7CA60}" @@ -35,6 +33,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Serializer.Experimen EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Examples.ExampleTODOApi", "examples\EdgeDB.Examples.ExampleTODOApi\EdgeDB.Examples.ExampleTODOApi.csproj", "{E38429C6-53A5-4311-8189-1F78238666DC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Net.CLI", "src\EdgeDB.Net.CLI\EdgeDB.Net.CLI.csproj", "{77D1980E-9835-4D16-B7B4-6CB5A4BD570C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CLI Generation Example", "CLI Generation Example", "{46E94884-7A3D-4B48-9734-916ECCCB85C0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Examples.GenerationExample", "examples\EdgeDB.Examples.GenerationExample\EdgeDB.Examples.GenerationExample.csproj", "{4BCAA352-8488-46A9-B3C9-D2C50E937AFC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EdgeDB.Generated", "examples\EdgeDB.Examples.GenerationExample\EdgeDB.Generated\EdgeDB.Generated.csproj", "{E03D7BD1-B093-44FF-B276-410345430523}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -61,10 +67,6 @@ Global {1557B745-EB4F-449A-9BE7-180C8990AD47}.Debug|Any CPU.Build.0 = Debug|Any CPU {1557B745-EB4F-449A-9BE7-180C8990AD47}.Release|Any CPU.ActiveCfg = Release|Any CPU {1557B745-EB4F-449A-9BE7-180C8990AD47}.Release|Any CPU.Build.0 = Release|Any CPU - {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD}.Release|Any CPU.Build.0 = Release|Any CPU {C189294A-4990-4A06-B120-A0AF03A798C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C189294A-4990-4A06-B120-A0AF03A798C6}.Debug|Any CPU.Build.0 = Debug|Any CPU {C189294A-4990-4A06-B120-A0AF03A798C6}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -85,6 +87,18 @@ Global {E38429C6-53A5-4311-8189-1F78238666DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {E38429C6-53A5-4311-8189-1F78238666DC}.Release|Any CPU.ActiveCfg = Release|Any CPU {E38429C6-53A5-4311-8189-1F78238666DC}.Release|Any CPU.Build.0 = Release|Any CPU + {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77D1980E-9835-4D16-B7B4-6CB5A4BD570C}.Release|Any CPU.Build.0 = Release|Any CPU + {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4BCAA352-8488-46A9-B3C9-D2C50E937AFC}.Release|Any CPU.Build.0 = Release|Any CPU + {E03D7BD1-B093-44FF-B276-410345430523}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E03D7BD1-B093-44FF-B276-410345430523}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E03D7BD1-B093-44FF-B276-410345430523}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E03D7BD1-B093-44FF-B276-410345430523}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -95,12 +109,15 @@ Global {CD4A4B5D-4B67-4E64-A13B-3F7BF770B056} = {E6B9FABC-241B-4561-9A94-E67B6BE380E2} {4CC2101C-D0AB-4F08-A0FD-205F96958196} = {025AAADF-16AF-4367-9C3D-9E60EDED832F} {1557B745-EB4F-449A-9BE7-180C8990AD47} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA} - {74DB9D5E-9BA9-4282-90EA-2F7BDC4C4FBD} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA} {C189294A-4990-4A06-B120-A0AF03A798C6} = {E6B9FABC-241B-4561-9A94-E67B6BE380E2} {5FFD1E88-614D-409B-8420-F9571AC7CA60} = {E6B9FABC-241B-4561-9A94-E67B6BE380E2} {3A4AAAA0-9948-43D3-B838-8EFAC130240C} = {67ED9EF0-7828-44C0-8CB0-DEBD69EC94CA} {6FA68DEA-D398-4A5B-8025-5F15C728F04C} = {49B6FB80-A675-4ECA-802C-2337A4F37566} {E38429C6-53A5-4311-8189-1F78238666DC} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B} + {77D1980E-9835-4D16-B7B4-6CB5A4BD570C} = {025AAADF-16AF-4367-9C3D-9E60EDED832F} + {46E94884-7A3D-4B48-9734-916ECCCB85C0} = {6FC214F5-C912-4D99-91B1-3E9F52A4E11B} + {4BCAA352-8488-46A9-B3C9-D2C50E937AFC} = {46E94884-7A3D-4B48-9734-916ECCCB85C0} + {E03D7BD1-B093-44FF-B276-410345430523} = {46E94884-7A3D-4B48-9734-916ECCCB85C0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4E90C94F-D693-4411-82F3-2051DE1BE052} diff --git a/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json b/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json index 9f782db0..0999bbcb 100644 --- a/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json +++ b/examples/EdgeDB.Examples.ExampleTODOApi/Properties/launchSettings.json @@ -1,4 +1,4 @@ -{ +{ "$schema": "https://json.schemastore.org/launchsettings.json", "iisSettings": { "windowsAuthentication": false, @@ -11,7 +11,6 @@ "profiles": { "EdgeDB.Examples.ExampleTODOApi": { "commandName": "Project", - "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "https://localhost:7155;http://localhost:5155", @@ -28,4 +27,4 @@ } } } -} +} \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj new file mode 100644 index 00000000..7ca5e342 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Examples.GenerationExample.csproj @@ -0,0 +1,20 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs new file mode 100644 index 00000000..0cf9674d --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/CreateUser.g.cs @@ -0,0 +1,34 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:B9692A5CA0A9992246361197BEACBDE398A5A30C5DCCC83BCACD8C80D5842FEB +// Generated on 2022-08-26T11:55:03.5378711Z +#nullable enable +using EdgeDB; + +namespace EdgeDB.Generated; + +#region Types +[EdgeDBType] +public sealed class CreateUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } +} + +#endregion + +public static class CreateUser +{ + public static readonly string Query = @"INSERT Person { + name := $name, + email := $email +} +UNLESS CONFLICT ON .email +ELSE (SELECT Person)"; + + public static Task ExecuteAsync(IEdgeDBQueryable client, String? name, String? email, CancellationToken token = default) + => client.QuerySingleAsync(Query, new Dictionary() { { "name", name }, { "email", email } }, capabilities: (Capabilities)1ul, token: token); + + public static Task CreateUserAsync(this IEdgeDBQueryable client, String? name, String? email, CancellationToken token = default) + => ExecuteAsync(client, name, email, token: token); +} +#nullable restore diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs new file mode 100644 index 00000000..07e05e9c --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/DeleteUser.g.cs @@ -0,0 +1,29 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:28E22B161B70DB5DE661AC86709BEADD3F9DC475559DE90F194CBAF24A5A880C +// Generated on 2022-08-28T12:51:13.6300794Z +#nullable enable +using EdgeDB; + +namespace EdgeDB.Generated; + +#region Types +[EdgeDBType] +public sealed class DeleteUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } +} + +#endregion + +public static class DeleteUser +{ + public static readonly string Query = @"delete Person filter .email = $email"; + + public static Task ExecuteAsync(IEdgeDBQueryable client, String? email, CancellationToken token = default) + => client.QuerySingleAsync(Query, new Dictionary() { { "email", email } }, capabilities: (Capabilities)1ul, token: token); + + public static Task DeleteUserAsync(this IEdgeDBQueryable client, String? email, CancellationToken token = default) + => ExecuteAsync(client, email, token: token); +} +#nullable restore diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj new file mode 100644 index 00000000..f4a98cb4 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/EdgeDB.Generated.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs new file mode 100644 index 00000000..7ba9436a --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/GetUser.g.cs @@ -0,0 +1,38 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:E116C5881529C70BFC6A9D0350075968AD967D391FEE0F74033FA870A1FE56DD +// Generated on 2022-08-26T11:52:03.5962767Z +#nullable enable +using EdgeDB; + +namespace EdgeDB.Generated; + +#region Types +[EdgeDBType] +public sealed class GetUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } + + [EdgeDBProperty("name")] + public String? Name { get; set; } + + [EdgeDBProperty("email")] + public String? Email { get; set; } +} + +#endregion + +public static class GetUser +{ + public static readonly string Query = @"select Person { + name, email +} +filter .email = $email"; + + public static Task ExecuteAsync(IEdgeDBQueryable client, String? email, CancellationToken token = default) + => client.QuerySingleAsync(Query, new Dictionary() { { "email", email } }, capabilities: (Capabilities)0ul, token: token); + + public static Task GetUserAsync(this IEdgeDBQueryable client, String? email, CancellationToken token = default) + => ExecuteAsync(client, email, token: token); +} +#nullable restore diff --git a/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs new file mode 100644 index 00000000..8023db81 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/EdgeDB.Generated/UpdateUser.g.cs @@ -0,0 +1,29 @@ +// AUTOGENERATED: DO NOT MODIFY +// edgeql:A1C5568E24B561F8D8CE4109C9E8E604C801BF104AFA3A4C07A0F2EDB720AC27 +// Generated on 2022-08-26T15:33:42.9431602Z +#nullable enable +using EdgeDB; + +namespace EdgeDB.Generated; + +#region Types +[EdgeDBType] +public sealed class UpdateUserResult +{ + [EdgeDBProperty("id")] + public Guid Id { get; set; } +} + +#endregion + +public static class UpdateUser +{ + public static readonly string Query = @"update Person filter .id = $id set { name := $name, email := $email }"; + + public static Task ExecuteAsync(IEdgeDBQueryable client, Guid id, String? name, String? email, CancellationToken token = default) + => client.QuerySingleAsync(Query, new Dictionary() { { "id", id }, { "name", name }, { "email", email } }, capabilities: (Capabilities)1ul, token: token); + + public static Task UpdateUserAsync(this IEdgeDBQueryable client, Guid id, String? name, String? email, CancellationToken token = default) + => ExecuteAsync(client, id, name, email, token: token); +} +#nullable restore diff --git a/examples/EdgeDB.Examples.GenerationExample/Program.cs b/examples/EdgeDB.Examples.GenerationExample/Program.cs new file mode 100644 index 00000000..6a530b2a --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Program.cs @@ -0,0 +1,11 @@ +using EdgeDB; +using EdgeDB.Generated; + +// create a client +var client = new EdgeDBClient(); + +// create a user +await client.CreateUserAsync(name: "example", email: "example@example.com"); + +// Get a user based on email +var user = await client.GetUserAsync(email: "example@example.com"); diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/CreateUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/CreateUser.edgeql new file mode 100644 index 00000000..e65e9a37 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/CreateUser.edgeql @@ -0,0 +1,6 @@ +INSERT Person { + name := $name, + email := $email +} +UNLESS CONFLICT ON .email +ELSE (SELECT Person) \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/DeleteUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/DeleteUser.edgeql new file mode 100644 index 00000000..5a7b4883 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/DeleteUser.edgeql @@ -0,0 +1 @@ +delete Person filter .email = $email \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/GetUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/GetUser.edgeql new file mode 100644 index 00000000..2d4f61b6 --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/GetUser.edgeql @@ -0,0 +1,4 @@ +select Person { + name, email +} +filter .email = $email \ No newline at end of file diff --git a/examples/EdgeDB.Examples.GenerationExample/Scrips/UpdateUser.edgeql b/examples/EdgeDB.Examples.GenerationExample/Scrips/UpdateUser.edgeql new file mode 100644 index 00000000..ded619fb --- /dev/null +++ b/examples/EdgeDB.Examples.GenerationExample/Scrips/UpdateUser.edgeql @@ -0,0 +1 @@ +update Person filter .id = $id set { name := $name, email := $email } \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs b/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs new file mode 100644 index 00000000..7bde6ef0 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Arguments/ConnectionArguments.cs @@ -0,0 +1,94 @@ +using CommandLine; +using EdgeDB.CLI.Utils; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Arguments +{ + public class ConnectionArguments : LogArgs + { + [Option("dsn", HelpText = "DSN for EdgeDB to connect to (overrides all other options except password)")] + public string? DSN { get; set; } + + [Option("credentials-file", HelpText = "Path to JSON file to read credentials from")] + public string? CredentialsFile { get; set; } + + [Option('I', "instance", HelpText = "Local instance name created with edgedb instance create to connect to (overrides host and port)")] + public string? Instance { get; set; } + + [Option('H', "host", HelpText = "Host of the EdgeDB instance")] + public string? Host { get; set; } + + [Option('P', "port", HelpText = "Port to connect to EdgeDB")] + public int? Port { get; set; } + + [Option('d', "database", HelpText = "Database name to connect to")] + public string? Database { get; set; } + + [Option('u', "user", HelpText = "User name of the EdgeDB user")] + public string? User { get; set; } + + [Option("password", HelpText = "Ask for password on the terminal (TTY)")] + public bool Password { get; set; } + + [Option("password-from-stdin", HelpText = "Read the password from stdin rather than TTY (useful for scripts)")] + public bool PasswordFromSTDIN { get; set; } + + [Option("tls-ca-file", HelpText = "Certificate to match server against\n\nThis might either be full self-signed server certificate or certificate authority (CA) certificate that server certificate is signed with.")] + public string? TLSCAFile { get; set; } + + [Option("tls-security", HelpText = "Specify the client-side TLS security mode.")] + public TLSSecurityMode? TLSSecurity { get; set; } + + public EdgeDBConnection GetConnection() + { + if (DSN is not null) + return EdgeDBConnection.FromDSN(DSN); + + if (Instance is not null) + return EdgeDBConnection.FromInstanceName(Instance); + + if (CredentialsFile is not null) + return JsonConvert.DeserializeObject(File.ReadAllText(CredentialsFile)) + ?? throw new NullReferenceException($"The file '{CredentialsFile}' didn't contain a valid credential definition"); + + // create the resolved connection + var resolved = EdgeDBConnection.ResolveConnection(); + + if (Host is not null) + resolved.Hostname = Host; + + if (Port.HasValue) + resolved.Port = Port.Value; + + if (Database is not null) + resolved.Database = Database; + + if (User is not null) + resolved.Username = User; + + if (Password) + { + // read password from console + Console.Write($"Password for '{resolved.Database}': "); + + resolved.Password = ConsoleUtils.ReadSecretInput(); + } + + if (PasswordFromSTDIN) + resolved.Password = Console.ReadLine(); + + if (TLSCAFile is not null) + resolved.TLSCertificateAuthority = TLSCAFile; + + if (TLSSecurity.HasValue) + resolved.TLSSecurity = TLSSecurity.Value; + + return resolved; + } + } +} diff --git a/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs b/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs new file mode 100644 index 00000000..b72ed64a --- /dev/null +++ b/src/EdgeDB.Net.CLI/Arguments/LogArgs.cs @@ -0,0 +1,23 @@ +using CommandLine; +using Microsoft.Extensions.Logging; +using Serilog.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Arguments +{ + /// + /// A class containing logger arguments. + /// + public class LogArgs + { + /// + /// Gets or sets the log level for the default logger. + /// + [Option("loglevel", HelpText = "Configure the log level")] + public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; + } +} diff --git a/src/EdgeDB.Net.CLI/Commands/FileWatch.cs b/src/EdgeDB.Net.CLI/Commands/FileWatch.cs new file mode 100644 index 00000000..c7ec55cf --- /dev/null +++ b/src/EdgeDB.Net.CLI/Commands/FileWatch.cs @@ -0,0 +1,260 @@ +using CommandLine; +using EdgeDB.CLI; +using EdgeDB.CLI.Arguments; +using EdgeDB.CLI.Utils; +using Newtonsoft.Json; +using Serilog; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Commands +{ + /// + /// A class representing the watch command. + /// + [Verb("watch", HelpText = "Configure the file watcher")] + public class FileWatch : ConnectionArguments, ICommand + { + /// + /// Gets or sets whether or not to kill a already running watcher. + /// + [Option('k', "kill", SetName = "functions", HelpText = "Kill the current running watcher for the project")] + public bool Kill { get; set; } + + /// + /// Gets or sets whether or not to start a watcher. + /// + [Option('s', "start", SetName = "functions", HelpText = "Start a watcher for the current project")] + public bool Start { get; set; } + + /// + /// Gets or sets the output directory to place the generated source files. + /// + [Option('o', "output", HelpText = "The output directory for the generated source to be placed.")] + public string? OutputDirectory { get; set; } + + /// + /// Gets or sets the project name/namespace of generated files. + /// + [Option('n', "project-name", HelpText = "The name of the generated project and namespace of generated files.")] + public string GeneratedProjectName { get; set; } = "EdgeDB.Generated"; + + /// + public Task ExecuteAsync(ILogger logger) + { + // get project root + var root = ProjectUtils.GetProjectRoot(); + var watcher = ProjectUtils.GetWatcherProcess(root); + logger.Debug("Watcher?: {@Watcher} Project root: {@Root}", watcher is not null, root); + try + { + if (!Kill && !Start) + { + // display information about the current watcher + if (watcher is null) + { + logger.Information("No file watcher is running for {@Dir}", root); + return Task.CompletedTask; + } + + logger.Information("File watcher is watching {@Dir}", Path.Combine(root, "*.edgeql")); + logger.Information("Process ID: {@Id}", watcher.Id); + + return Task.CompletedTask; + } + + if (Kill) + { + if (watcher is null) + { + logger.Error("No watcher is running for {@Dir}", root); + return Task.CompletedTask; + } + + watcher.Kill(); + logger.Information("Watcher process {@PID} kiled", watcher.Id); + + return Task.CompletedTask; + } + + if (Start) + { + if (watcher is not null) + { + logger.Error("Watcher already running! Process ID: {@PID}", watcher.Id); + return Task.CompletedTask; + } + + var connection = GetConnection(); + + OutputDirectory ??= Path.Combine(Environment.CurrentDirectory, GeneratedProjectName); + + var pid = ProjectUtils.StartWatchProcess(connection, root, OutputDirectory, GeneratedProjectName); + + logger.Information("Watcher process started, PID: {@PID}", pid); + } + + return Task.CompletedTask; + } + catch(Exception x) + { + logger.Error(x, "Failed to run watcher command"); + return Task.CompletedTask; + } + } + } + + [Verb("file-watch-internal", Hidden = true)] + internal class FileWatchInternal : ICommand + { + [Option("connection")] + public string? Connection { get; set; } + + [Option("dir")] + public string? Dir { get; set; } + + [Option("output")] + public string? Output { get; set; } + + [Option("namespace")] + public string? Namespace { get; set; } + + private readonly FileSystemWatcher _watcher = new(); + private EdgeDBTcpClient? _client; + private readonly SemaphoreSlim _mutex = new(1, 1); + private readonly ConcurrentStack _writeQueue = new(); + private TaskCompletionSource _writeDispatcher = new(); + + public async Task ExecuteAsync(ILogger logger) + { + if (Connection is null) + throw new InvalidOperationException("Connection must be specified"); + + _client = new(JsonConvert.DeserializeObject(Connection)!, new()); + + _watcher.Path = Dir!; + _watcher.Filter = "*.edgeql"; + _watcher.IncludeSubdirectories = true; + + _watcher.Error += _watcher_Error; + + _watcher.Changed += CreatedAndUpdated; + _watcher.Created += CreatedAndUpdated; + _watcher.Deleted += _watcher_Deleted; + _watcher.Renamed += _watcher_Renamed; + + _watcher.NotifyFilter = NotifyFilters.Attributes + | NotifyFilters.CreationTime + | NotifyFilters.DirectoryName + | NotifyFilters.FileName + | NotifyFilters.LastAccess + | NotifyFilters.LastWrite + | NotifyFilters.Security + | NotifyFilters.Size; + + _watcher.EnableRaisingEvents = true; + + ProjectUtils.RegisterProcessAsWatcher(Dir!); + + logger.Information("Watcher started for {@Dir}", Output); + + while (true) + { + await _writeDispatcher.Task; + + while(_writeQueue.TryPop(out var info)) + { + await GenerateAsync(info); + } + + _writeDispatcher = new(); + } + } + + private bool IsValidFile(string path) + => !path.StartsWith(Path.Combine(Dir!, "dbschema", "migrations")); + + public async Task GenerateAsync(EdgeQLParser.GenerationTargetInfo info) + { + await _mutex.WaitAsync().ConfigureAwait(false); + + try + { + // check if the file is a valid file + if (!IsValidFile(info.EdgeQLFilePath!)) + return; + + await _client!.ConnectAsync(); + + try + { + var result = await EdgeQLParser.ParseAndGenerateAsync(_client, Namespace!, info); + File.WriteAllText(info.TargetFilePath!, result.Code); + } + catch (EdgeDBErrorException err) + { + // error with file + Console.WriteLine(err.Message); + } + } + catch(Exception x) + { + Console.WriteLine(x); + } + finally + { + _mutex.Release(); + } + } + + private void CreatedAndUpdated(object sender, FileSystemEventArgs e) + { + if (!FileUtils.WaitForHotFile(e.FullPath)) + return; + + // wait an extra second to make sure the file is fully written + Thread.Sleep(1000); + + var info = EdgeQLParser.GetTargetInfo(e.FullPath, Output!); + + if (info.IsGeneratedTargetExistsAndIsUpToDate()) + return; + + _writeQueue.Push(info); + _writeDispatcher.TrySetResult(); + } + + private void _watcher_Deleted(object sender, FileSystemEventArgs e) + { + if (!IsValidFile(e.FullPath)) + return; + + // get the generated file name + var path = Path.Combine(Output!, $"{Path.GetFileNameWithoutExtension(e.FullPath)}.g.cs"); + + if (File.Exists(path)) + File.Delete(path); + } + + private void _watcher_Renamed(object sender, RenamedEventArgs e) + { + if (!IsValidFile(e.FullPath)) + return; + + var oldPath = Path.Combine(Output!, $"{Path.GetFileNameWithoutExtension(e.OldFullPath)}.g.cs"); + var newPath = Path.Combine(Output!, $"{Path.GetFileNameWithoutExtension(e.FullPath)}.g.cs"); + + if (File.Exists(oldPath)) + File.Move(oldPath, newPath); + } + + private void _watcher_Error(object sender, ErrorEventArgs e) + { + Console.Error.WriteLine($"An error occored: {e.GetException()}"); + } + } +} diff --git a/src/EdgeDB.Net.CLI/Commands/Generate.cs b/src/EdgeDB.Net.CLI/Commands/Generate.cs new file mode 100644 index 00000000..d795437c --- /dev/null +++ b/src/EdgeDB.Net.CLI/Commands/Generate.cs @@ -0,0 +1,139 @@ +using CommandLine; +using EdgeDB.CLI.Arguments; +using EdgeDB.CLI.Utils; +using EdgeDB.Codecs; +using Newtonsoft.Json; +using Serilog; +using System.Diagnostics; +using System.Reflection; +using System.Text.RegularExpressions; + +namespace EdgeDB.CLI; + +/// +/// A class representing the generate command. +/// +[Verb("generate", HelpText = "Generate or updates csharp classes from .edgeql files.")] +public class Generate : ConnectionArguments, ICommand +{ + /// + /// Gets or sets whether or not a class library should be generated. + /// + [Option('p', "project", HelpText = "Whether or not to create the default class library that will contain the generated source code. Enabled by default.")] + public bool GenerateProject { get; set; } = true; + + /// + /// Gets or sets the output directory the generated source files will be placed. + /// + [Option('o', "output", HelpText = "The output directory for the generated source to be placed. When generating a project, source files will be placed in that projects directory. Default is the current directory")] + public string? OutputDirectory { get; set; } + + /// + /// Gets or sets the project name/namespace. + /// + [Option('n', "project-name", HelpText = "The name of the generated project and namespace of generated files.")] + public string GeneratedProjectName { get; set; } = "EdgeDB.Generated"; + + /// + /// Gets or sets whether or not to force (re)generate source files. + /// + [Option('f', "force", HelpText = "Force regeneration of files")] + public bool Force { get; set; } + + /// + /// Gets or sets whether or not to start a watch process post-generate. + /// + [Option("watch", HelpText = "Listens for any changes or new edgeql files and (re)generates them automatically")] + public bool Watch { get; set; } + + /// + public async Task ExecuteAsync(ILogger logger) + { + // get connection info + var connection = GetConnection(); + + // create the client + var client = new EdgeDBTcpClient(connection, new()); + + logger.Information("Connecting to {@Host}:{@Port}...", connection.Hostname, connection.Port); + await client.ConnectAsync(); + + var projectRoot = ProjectUtils.GetProjectRoot(); + + OutputDirectory ??= Environment.CurrentDirectory; + + Directory.CreateDirectory(OutputDirectory); + + if (GenerateProject && !Directory.Exists(Path.Combine(OutputDirectory, GeneratedProjectName))) + { + logger.Information("Creating project {@ProjectName}...", GeneratedProjectName); + await ProjectUtils.CreateGeneratedProjectAsync(OutputDirectory, GeneratedProjectName); + } + + if(GenerateProject) + OutputDirectory = Path.Combine(OutputDirectory, GeneratedProjectName); + + // find edgeql files + var edgeqlFiles = ProjectUtils.GetTargetEdgeQLFiles(projectRoot).ToArray(); + + // error if any are the same name + var groupFileNames = edgeqlFiles.GroupBy(x => Path.GetFileNameWithoutExtension(x)); + if(groupFileNames.Any(x => x.Count() > 1)) + { + foreach(var conflict in groupFileNames.Where(x => x.Count() > 1)) + { + logger.Fatal($"{{@Count}} files contain the same name ({string.Join(" - ", conflict.Select(x => x))})", conflict.Count()); + } + + return; + } + + logger.Information("Generating {@FileCount} files...", edgeqlFiles.Length); + + for(int i = 0; i != edgeqlFiles.Length; i++) + { + var file = edgeqlFiles[i]; + var info = EdgeQLParser.GetTargetInfo(file, OutputDirectory); + + if (!Force && info.IsGeneratedTargetExistsAndIsUpToDate()) + { + logger.Warning("Skipping {@File}: File already generated and up-to-date.", file); + continue; + } + + try + { + var result = await EdgeQLParser.ParseAndGenerateAsync(client, GeneratedProjectName, info); + File.WriteAllText(info.TargetFilePath!, result.Code); + } + catch (EdgeDBErrorException error) + { + logger.Error("Skipping {@File}: Failed to parse - {@Message} at line {@Line} column {@Column}", + file, + error.Message, + error.ErrorResponse.Attributes.FirstOrDefault(x => x.Code == 65523).ToString() ?? "??", + error.ErrorResponse.Attributes.FirstOrDefault(x => x.Code == 65524).ToString() ?? "??"); + continue; + } + + logger.Debug("{@EdgeQL} => {@CSharp}", file, info.TargetFilePath); + } + + logger.Information("Generation complete!"); + + if(Watch) + { + var existing = ProjectUtils.GetWatcherProcess(projectRoot); + + if(existing is not null) + { + logger.Warning("Watching already running"); + return; + } + + logger.Information("Starting file watcher..."); + var pid = ProjectUtils.StartWatchProcess(connection, projectRoot, OutputDirectory, GeneratedProjectName); + logger.Information("File watcher process started, PID: {@PID}", pid); + } + } +} \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj b/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj new file mode 100644 index 00000000..cfc8deef --- /dev/null +++ b/src/EdgeDB.Net.CLI/EdgeDB.Net.CLI.csproj @@ -0,0 +1,26 @@ + + + + Exe + net6.0 + enable + enable + EdgeDB.Net.CLI + EdgeDB + A CLI tool to generate C# files from edgeql files + edgeql + true + + + + + + + + + + + + + + diff --git a/src/EdgeDB.Net.CLI/EdgeQLParser.cs b/src/EdgeDB.Net.CLI/EdgeQLParser.cs new file mode 100644 index 00000000..72c761f5 --- /dev/null +++ b/src/EdgeDB.Net.CLI/EdgeQLParser.cs @@ -0,0 +1,557 @@ +using EdgeDB.CLI.Utils; +using EdgeDB.Codecs; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EdgeDB.CLI +{ + /// + /// Represents a class responsible for parsing and transpiling edgeql to C#. + /// + internal class EdgeQLParser + { + /// + /// The file header regex for generate C# files. + /// + private static readonly Regex _headerHashRegex = new(@"\/\/ edgeql:([0-9a-fA-F]{64})"); + + /// + /// Parses and generates a from a given client and + /// . + /// + /// The client to preform the parse with. + /// The namespace for the . + /// + /// The information containimg the edgeql and related + /// content used to parse and generate. + /// + /// A containing the generated C# code, hash, + /// classname, and executer name. + /// + public static async Task ParseAndGenerateAsync(EdgeDBTcpClient client, string @namespace, GenerationTargetInfo targetInfo) + { + var parseResult = await client.ParseAsync(targetInfo.EdgeQL!, Cardinality.Many, IOFormat.Binary, Capabilities.All, default); + + return GenerateCSharpFromEdgeQL(@namespace, targetInfo, parseResult); + } + + /// + /// Checks whether an autogenerate header matches a hash. + /// + /// The header of the autogenerated file to check against. + /// The hash to check. + /// + /// if the header matches the hash; otherwise . + /// + public static bool TargetFileHashMatches(string header, string hash) + { + var match = _headerHashRegex.Match(header); + if (!match.Success) + return false; + return match.Groups[1].Value == hash; + } + + /// + /// Gets the for the given file and + /// generation target directory. + /// + /// + /// This operation requires the file to be opened. This function will + /// throw if the file is being used by a different process. + /// + /// The path of the edgeql file. + /// The output target directory. + /// + /// The for the given file. + /// + public static GenerationTargetInfo GetTargetInfo(string edgeqlFilePath, string targetDir) + { + string fileContent = File.ReadAllText(edgeqlFilePath); + var hash = HashUtils.HashEdgeQL(fileContent); + var fileName = TextUtils.ToPascalCase(Path.GetFileName(edgeqlFilePath).Split('.')[0]); + + return new GenerationTargetInfo + { + EdgeQLFileNameWithoutExtension = fileName, + EdgeQL = fileContent, + EdgeQLHash = hash, + EdgeQLFilePath = edgeqlFilePath, + TargetFilePath = Path.Combine(targetDir, $"{fileName}.g.cs") + }; + } + + /// + /// Represents a generation target, containing useful information for it. + /// + public class GenerationTargetInfo + { + /// + /// Gets or sets the edgeql file name without extension. + /// + public string? EdgeQLFileNameWithoutExtension { get; set; } + + /// + /// Gets or sets the edgeql file path. + /// + public string? EdgeQLFilePath { get; set; } + + /// + /// Gets or sets the output target file path. + /// + public string? TargetFilePath { get; set; } + + /// + /// Gets or sets the edgeql. + /// + public string? EdgeQL { get; set; } + + /// + /// Gets or sets the hash of the edgeql. + /// + public string? EdgeQLHash { get; set; } + + /// + /// Checks if the target file exists and the header matches the hash of the edgeql. + /// + /// + public bool IsGeneratedTargetExistsAndIsUpToDate() + { + var lines = File.Exists(TargetFilePath) ? File.ReadAllLines(TargetFilePath) : Array.Empty(); + + return File.Exists(TargetFilePath) && lines.Length >= 2 && TargetFileHashMatches(lines[1], EdgeQLHash!); + } + } + + /// + /// A class representing the result of a edgeql -> cs generation operation. + /// + public class GenerationResult + { + /// + /// Gets the code generated from edgeql. + /// + public string? Code { get; set; } + + /// + /// Gets the hash of the edgeql and header of the generated code. + /// + public string? EdgeQLHash { get; set; } + + /// + /// Gets the name of the class containing the execute method. + /// + public string? ExecuterClassName { get; set; } + + /// + /// Gets the name of the return result that the executer method returns. + /// + public string? ReturnResult { get; set; } + + /// + /// Gets a collection of parameters (edgeql arguments) for the executer function. + /// + public IEnumerable? Parameters { get; set; } + } + + /// + /// Generates a from the given + /// and . + /// + /// The namepsace for the generated code to consume. + /// The used for generation. + /// The parse result from edgedb. + /// + /// + private static GenerationResult GenerateCSharpFromEdgeQL(string @namespace, GenerationTargetInfo targetInfo, EdgeDBBinaryClient.ParseResult parseResult) + { + var codecType = GetTypeInfoFromCodec(parseResult.OutCodec.Codec, $"{targetInfo.EdgeQLFileNameWithoutExtension} Result"); + + // create the class writer + + var writer = new CodeWriter(); + writer.AppendLine("// AUTOGENERATED: DO NOT MODIFY"); + writer.AppendLine($"// edgeql:{targetInfo.EdgeQLHash}"); + writer.AppendLine($"// Generated on {DateTime.UtcNow:O}"); + writer.AppendLine("#nullable enable"); + writer.AppendLine($"using EdgeDB;"); + writer.AppendLine(); + writer.AppendLine($"namespace {@namespace};"); + writer.AppendLine(); + + var refTypes = new List(); + var compliledTypes = new List<(CodecTypeInfo Info, string Reference)?>(); + + var mainResult = BuildTypes(codecType, out var typeName, refTypes); + + compliledTypes.Add((codecType, mainResult)); + + if (refTypes.Any() || codecType.IsObject) + { + var seenTypes = new HashSet(refTypes) { codecType }; + var refStack = new Stack(refTypes); + + writer.AppendLine("#region Types"); + writer.AppendLine(mainResult); + + // circle dependency safe! + while (refStack.TryPop(out var typeInfo)) + { + var complRef = compliledTypes.FirstOrDefault(x => x!.Value.Info.BodyEquals(typeInfo)); + + if (complRef is not null) + { + writer.AppendLine(complRef.Value.Reference); + continue; + } + + var newTypes = new List(); + var result = BuildTypes(typeInfo, out _, newTypes); + + if (newTypes.Any()) + foreach (var newType in newTypes.Where(x => !seenTypes.TryGetValue(x, out _))) + refStack.Push(newType); + + writer.AppendLine(result); + compliledTypes.Add((typeInfo, result)); + } + + writer.AppendLine("#endregion"); + writer.AppendLine(); + } + + // create the executor class + var classScope = writer.BeginScope($"public static class {targetInfo.EdgeQLFileNameWithoutExtension}"); + + writer.AppendLine($"public static readonly string Query = @\"{targetInfo.EdgeQL}\";"); + writer.AppendLine(); + var method = parseResult.Cardinality switch + { + Cardinality.AtMostOne => "QuerySingleAsync", + Cardinality.One => "QueryRequiredSingleAsync", + _ => "QueryAsync" + }; + + var resultType = parseResult.Cardinality switch + { + Cardinality.AtMostOne => $"{typeName ?? mainResult}?", + Cardinality.One => typeName ?? mainResult, + _ => $"IReadOnlyCollection<{typeName ?? mainResult}?>" + }; + + // build args + IEnumerable? argParameters; + IEnumerable? methodArgs; + if (parseResult.InCodec.Codec is NullCodec) + { + methodArgs = Array.Empty(); + argParameters = Array.Empty(); + } + else if (parseResult.InCodec.Codec is Codecs.Object argCodec) + { + argParameters = argParameters = argCodec.PropertyNames.Select((x, i) => BuildTypes(GetTypeInfoFromCodec(argCodec.InnerCodecs[i], x), out _, namesOnScalar: true, camelCase: true)); + methodArgs = methodArgs = argCodec.PropertyNames.Select((x, i) => + { + return $"{{ \"{x}\", {TextUtils.ToCamelCase(x)} }}"; + }); + } + else + throw new InvalidOperationException("Argument codec is malformed"); + + writer.AppendLine($"public static Task<{resultType}> ExecuteAsync(IEdgeDBQueryable client{(argParameters.Any() ? $", {string.Join(", ", argParameters)}" : "")}, CancellationToken token = default)"); + writer.AppendLine($" => client.{method}<{typeName ?? mainResult}>(Query{(methodArgs.Any() ? $", new Dictionary() {{ {string.Join(", ", methodArgs)} }}" : "")}, capabilities: (Capabilities){(ulong)parseResult.Capabilities}ul, token: token);"); + + writer.AppendLine(); + writer.AppendLine($"public static Task<{resultType}> {targetInfo.EdgeQLFileNameWithoutExtension}Async(this IEdgeDBQueryable client{(argParameters.Any() ? $", {string.Join(", ", argParameters)}" : "")}, CancellationToken token = default)"); + writer.AppendLine($" => ExecuteAsync(client{(argParameters.Any() ? $", {string.Join(", ", argParameters.Select(x => x.Split(' ')[1]))}" : "")}, token: token);"); + + classScope.Dispose(); + + writer.AppendLine("#nullable restore"); + + return new() + { + ExecuterClassName = targetInfo.EdgeQLFileNameWithoutExtension, + EdgeQLHash = targetInfo.EdgeQLHash, + ReturnResult = resultType, + Parameters = argParameters, + Code = writer.ToString() + }; + } + + /// + /// Builds the C# equivalent type from the given . + /// + /// The codec info containing the type information to build. + /// The name of the type or scalar. + /// + /// The reference to a list of used sub-objects, use to keep track of generated + /// reference types. + /// + /// + /// Whether or not to include the on the generated + /// result. + /// + /// + /// Whether or not names will be camel case () or + /// pascal case (). + /// + /// + /// Whether or not to return the type name of objects without generating them. When + /// this is , the object info is added to the + /// . + /// + /// + /// The generated C# of the provided . + /// + /// The is unknown. + private static string BuildTypes(CodecTypeInfo info, out string? resultTypeName, + List? usedObjects = null, bool namesOnScalar = false, bool camelCase = false, + bool returnTypeName = false) + { + usedObjects ??= new(); + var writer = new CodeWriter(); + resultTypeName = null; + + var fmtName = info.Name is not null + ? camelCase + ? TextUtils.ToCamelCase(info.Name) + : TextUtils.ToPascalCase(info.Name) + : null; + + if (info.IsObject) + { + if (returnTypeName) + { + // add to used objects + usedObjects.Add(info); + if (namesOnScalar) + return $"{info.GetUniqueTypeName()}? {fmtName}"; + return fmtName!; + } + + // create the main class + writer.AppendLine("[EdgeDBType]"); + writer.AppendLine($"public sealed class {info.GetUniqueTypeName()}"); + using (_ = writer.BeginScope()) + { + var properties = info.Children!.Select(x => + { + var result = BuildTypes(x, out _, usedObjects, namesOnScalar: true, returnTypeName: true); + return $"[EdgeDBProperty(\"{x.Name}\")]{Environment.NewLine} public {result} {{ get; set; }}"; + }); + + writer.AppendLine(string.Join($"{Environment.NewLine}{Environment.NewLine} ", properties)); + } + + resultTypeName = info.TypeName!; + return writer.ToString(); + } + + if (info.IsTuple) + { + var types = info.Children!.Select(x => BuildTypes(x, out _, usedObjects, true)); + return $"({string.Join(", ", types)}){(namesOnScalar ? $" {fmtName}" : "")}"; + } + + if (info.IsArray) + { + var result = BuildTypes(info.Children!.Single(), out _, usedObjects, true); + return $"{result}[]{(namesOnScalar ? $" {fmtName}" : "")}"; + } + + if (info.IsSet) + { + var result = BuildTypes(info.Children!.Single(), out _, usedObjects, true); + return $"IEnumerable<{result}>{(namesOnScalar ? $" {fmtName}" : "")}"; + } + + + if (info.TypeName is not null) + return $"{info.TypeName}{(namesOnScalar ? $" {fmtName}" : "")}"; + + throw new InvalidOperationException($"Unknown type def {info}"); + } + + /// + /// Creates a from the given . + /// + /// The codec to get the type info for. + /// The optional name of the codec. + /// The optional parent of the codec. + /// + /// A representing type information about the provided codec. + /// + /// + /// No could be created from the provided codec. + /// + private static CodecTypeInfo GetTypeInfoFromCodec(ICodec codec, string? name = null, CodecTypeInfo? parent = null) + { + CodecTypeInfo info; + + switch (codec) + { + case Codecs.Object obj: + { + info = new CodecTypeInfo + { + IsObject = true, + TypeName = TextUtils.ToPascalCase(name!) + }; + info.Children = obj.InnerCodecs + .Select((x, i) => + obj.PropertyNames[i] is "__tname__" or "__tid__" + ? null + : GetTypeInfoFromCodec(x, obj.PropertyNames[i], info)) + .Where(x => x is not null)!; + } + break; + case ICodec set when ReflectionUtils.IsSubclassOfRawGeneric(typeof(Set<>), set.GetType()): + { + info = new CodecTypeInfo + { + IsArray = true, + }; + info.Children = new[] + { + GetTypeInfoFromCodec((ICodec)set.GetType().GetField("_innerCodec", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(set)!, parent: info) + }; + } + break; + case ICodec array when ReflectionUtils.IsSubclassOfRawGeneric(typeof(Array<>), array.GetType()): + { + info = new CodecTypeInfo + { + IsSet = true, + }; + info.Children = new[] + { + GetTypeInfoFromCodec((ICodec)array.GetType().GetField("_innerCodec", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(array)!, parent: info) + }; + } + break; + case Codecs.Tuple tuple: + { + info = new CodecTypeInfo + { + IsTuple = true, + }; + info.Children = tuple.InnerCodecs.Select(x => GetTypeInfoFromCodec(x, parent: info)); + } + break; + case ICodec scalar when ReflectionUtils.IsSubclassOfInterfaceGeneric(typeof(IScalarCodec<>), codec!.GetType()): + { + info = new CodecTypeInfo + { + TypeName = $"{codec.GetType().GetInterface("IScalarCodec`1")!.GetGenericArguments()[0].Name}{(codec.GetType().GetInterface("IScalarCodec`1")!.GetGenericArguments()[0].IsValueType ? "" : "?")}", + }; + } + break; + default: + throw new InvalidOperationException($"Unknown codec {codec}"); + } + + info.Name = name ?? info.Name; + info.Parent = parent; + + return info; + } + + /// + /// Represents an expanded form of a , containing + /// ease-to-parse information about a codec. + /// + private class CodecTypeInfo + { + /// + /// Gets whether or not the codec represents an array. + /// + public bool IsArray { get; init; } + + /// + /// Gets whether or not the codec represents a set. + /// + public bool IsSet { get; init; } + + /// + /// Gets whether or not the codec represents an object. + /// + public bool IsObject { get; init; } + + /// + /// Gets whether or not the codec represents a tuple. + /// + public bool IsTuple { get; init; } + + /// + /// Gets or sets the optional name of the codec. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the optional dotnet type name that represents what the + /// codec serializes/deserializes. + /// + public string? TypeName { get; set; } + + /// + /// Gets or sets the child s for this parent . + /// + public IEnumerable? Children { get; set; } + + /// + /// Gets the paret . + /// + public CodecTypeInfo? Parent { get; set; } + + /// + /// Checks whether or not the current 's body is equal to + /// the given . + /// + /// + /// The to check against. + /// + /// + /// if the 's body matches the + /// current ; otherwise . + /// + public bool BodyEquals(CodecTypeInfo info) + { + return IsArray == info.IsArray && + IsSet == info.IsSet && + IsObject == info.IsObject && + IsTuple == info.IsTuple && + (info.Children?.SequenceEqual(Children ?? Array.Empty()) ?? false); + } + + /// + /// Gets a unique name for the current . + /// + /// + /// A unique name representing the current . + /// + public string GetUniqueTypeName() + { + List path = new() { TypeName }; + var p = Parent; + while (p is not null) + { + path.Add(p.TypeName); + p = p.Parent; + } + path.Reverse(); + return string.Join("", path.Where(x => x is not null)); + } + + /// + public override string ToString() + { + return $"{Name} ({TypeName})"; + } + } + } +} diff --git a/src/EdgeDB.Net.CLI/ICommand.cs b/src/EdgeDB.Net.CLI/ICommand.cs new file mode 100644 index 00000000..3d1986ad --- /dev/null +++ b/src/EdgeDB.Net.CLI/ICommand.cs @@ -0,0 +1,18 @@ +using Serilog; + +namespace EdgeDB.CLI; + +/// +/// Represents a generic command that can be executed. +/// +interface ICommand +{ + /// + /// Executes the command, awaiting its completion. + /// + /// The logger for the command. + /// + /// A task that represents the execution flow of the command. + /// + Task ExecuteAsync(ILogger logger); +} \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/Program.cs b/src/EdgeDB.Net.CLI/Program.cs new file mode 100644 index 00000000..83d5ffa1 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Program.cs @@ -0,0 +1,65 @@ +using CommandLine; +using CommandLine.Text; +using EdgeDB.CLI; +using EdgeDB.CLI.Arguments; +using Serilog; + +// intialize our logger +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Console() + .CreateLogger(); + +// find all types that extend the 'ICommand' interface. +var commands = typeof(Program).Assembly.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(ICommand))); + +// create our command line arg parser with no default help writer. +var parser = new Parser(x => +{ + x.HelpWriter = null; +}); + +// parse the 'args'. +var result = parser.ParseArguments(args, commands.ToArray()); + +try +{ + // execute the parsed result if it is a command. + var commandResult = await result.WithParsedAsync(x => + { + // if the command supports log args, change the log level for our logger. + if(x is LogArgs logArgs) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Is(logArgs.LogLevel) + .WriteTo.Console() + .CreateLogger(); + } + + // execute the command with the logger. + return x.ExecuteAsync(Log.Logger); + }); + + // if the result was not parsed to a valid command. + result.WithNotParsed(err => + { + // build the help text. + var helpText = HelpText.AutoBuild(commandResult, h => + { + h.AdditionalNewLineAfterOption = true; + h.Heading = "EdgeDB.Net CLI"; + h.Copyright = "Copyright (c) 2022 EdgeDB"; + + return h; + }, e => e, verbsIndex: true); + + // write out the help text. + Console.WriteLine(helpText); + }); + +} +catch (Exception x) +{ + // log the root exception. + Log.Logger.Fatal(x, "Critical error"); +} \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/Properties/launchSettings.json b/src/EdgeDB.Net.CLI/Properties/launchSettings.json new file mode 100644 index 00000000..a30ce718 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "EdgeDB.Net.CLI": { + "commandName": "Project", + "commandLineArgs": "watch -k" + //"commandLineArgs": "watch -start -o C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample\\EdgeDB.Generated" + //"commandLineArgs": "file-watch-internal --dir C:\\Users\\lynch\\source\\repos\\EdgeDB --output C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample --namespace EdgeDB.Generated --connection \"{\\\"user\\\":\\\"edgedb\\\",\\\"password\\\":\\\"x3WnskHEbmXBvnT1Vf14ABuL\\\",\\\"Hostname\\\":\\\"127.0.0.1\\\",\\\"port\\\":10711,\\\"database\\\":\\\"edgedb\\\",\\\"tls_cert_data\\\":\\\"-----BEGIN CERTIFICATE-----\nMIIC0zCCAbugAwIBAgIRAPQegLaiakFsl0hPzNUeEaAwDQYJKoZIhvcNAQELBQAw\nGDEWMBQGA1UEAwwNRWRnZURCIFNlcnZlcjAeFw0yMjA3MjIxMzA1MzBaFw00MTA5\nMjExMzA1MzBaMBgxFjAUBgNVBAMMDUVkZ2VEQiBTZXJ2ZXIwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDOSRrWTy9oC9J7CWvGKg9Rg7o/R1D4AQO++rML\npSBd1obrSlJB/p5kLWsMuSM29dgRubqqY7PrrjvcDedpDwyAp9JeIz6tIkkqo+/d\n8dRCaV/3wHa5BgfN8fEE6blnmdrQ3asuVafZ989+qoa1PjLc9iU5JJgp/gb/SS7Z\n02L+0y+vw/QjvQcRoiDfCcIfasp6Z2Jor9RCd+AchNG7m0teQ2iK9BXG33QJUXdA\nYwXEALQxJCi23v197xZQUJbXlxh4+YIFJiYA95gXzoB/lBUo/CQSNwxzkofwLtV/\nWT+CmAP25D/v6/rLWi+ps+vM5GFcIetMK0Bk8yzkr+EXB07/AgMBAAGjGDAWMBQG\nA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAUDNnRYoHCtW5\nRMBBf0YTr9zpLA69cK9J6OfY7wPW1ROd3/qmPz57fJ1DOq+5R4U8+rAvk6pO5p6l\nLG3RyQrC5bJuhyxxcBMkc43INC641gMrJlMm4SSRI0HO1nYiZLFdyUjTfrH//ThA\nFVDwAFR/Xw97R1qZQdZYLhxdnRjYIySWUs8yBljtuEB5iorDX9AaXQ25kZ4f8eTE\nsmxezVKQks+FUPdX6yhb++uWvqqzK2flWuiztZmQWLCQnydKuwyBx4izhsiSZ3wL\nbdO8MaTbq2o2dzvzQAWxsf8Wg1Xogep4PA0goquTJvgjzcHuEtyl8404Mb4U/Fpy\ngIFB7zA3bA==\n-----END CERTIFICATE-----\n\\\",\\\"tls_ca\\\":\\\"-----BEGIN CERTIFICATE-----\nMIIC0zCCAbugAwIBAgIRAPQegLaiakFsl0hPzNUeEaAwDQYJKoZIhvcNAQELBQAw\nGDEWMBQGA1UEAwwNRWRnZURCIFNlcnZlcjAeFw0yMjA3MjIxMzA1MzBaFw00MTA5\nMjExMzA1MzBaMBgxFjAUBgNVBAMMDUVkZ2VEQiBTZXJ2ZXIwggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQDOSRrWTy9oC9J7CWvGKg9Rg7o/R1D4AQO++rML\npSBd1obrSlJB/p5kLWsMuSM29dgRubqqY7PrrjvcDedpDwyAp9JeIz6tIkkqo+/d\n8dRCaV/3wHa5BgfN8fEE6blnmdrQ3asuVafZ989+qoa1PjLc9iU5JJgp/gb/SS7Z\n02L+0y+vw/QjvQcRoiDfCcIfasp6Z2Jor9RCd+AchNG7m0teQ2iK9BXG33QJUXdA\nYwXEALQxJCi23v197xZQUJbXlxh4+YIFJiYA95gXzoB/lBUo/CQSNwxzkofwLtV/\nWT+CmAP25D/v6/rLWi+ps+vM5GFcIetMK0Bk8yzkr+EXB07/AgMBAAGjGDAWMBQG\nA1UdEQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAUDNnRYoHCtW5\nRMBBf0YTr9zpLA69cK9J6OfY7wPW1ROd3/qmPz57fJ1DOq+5R4U8+rAvk6pO5p6l\nLG3RyQrC5bJuhyxxcBMkc43INC641gMrJlMm4SSRI0HO1nYiZLFdyUjTfrH//ThA\nFVDwAFR/Xw97R1qZQdZYLhxdnRjYIySWUs8yBljtuEB5iorDX9AaXQ25kZ4f8eTE\nsmxezVKQks+FUPdX6yhb++uWvqqzK2flWuiztZmQWLCQnydKuwyBx4izhsiSZ3wL\nbdO8MaTbq2o2dzvzQAWxsf8Wg1Xogep4PA0goquTJvgjzcHuEtyl8404Mb4U/Fpy\ngIFB7zA3bA==\n-----END CERTIFICATE-----\n\\\",\\\"tls_security\\\":0}\"" + //"commandLineArgs": "generate -o C:\\Users\\lynch\\source\\repos\\EdgeDB\\examples\\EdgeDB.Examples.GenerationExample --watch" + } + } +} \ No newline at end of file diff --git a/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs b/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs new file mode 100644 index 00000000..2bcd71e3 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/CodeWriter.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EdgeDB.CLI +{ + /// + /// A utility class for writing code. + /// + internal class CodeWriter + { + /// + /// The content of the code writer. + /// + public readonly StringBuilder Content = new(); + + /// + /// Gets the indentation level of the current code writer. + /// + public int IndentLevel { get; private set; } + + /// + /// The scope tracker providing an implementation of IDisposable. + /// + private readonly ScopeTracker _scopeTracker; // We only need one. It can be reused. + + /// + /// Creates a new . + /// + public CodeWriter() + { + _scopeTracker = new(this); + } + + /// + /// Appends a string to the current code writer. + /// + /// The line to append + public void Append(string line) + => Content.Append(line); + + /// + /// Appends a line to the current code writer, respecting + /// and ending in a line terminator. + /// + /// The line to append + public void AppendLine(string line) + => Content.Append(new string(' ', IndentLevel)).AppendLine(line); + + /// + /// Appends an empty line to the current code writer, adding a line terminator to the end. + /// + public void AppendLine() + => Content.AppendLine(); + + /// + /// Begins a new scope with the specified line. + /// + /// The line to append. + /// An representing the scope returned. + public IDisposable BeginScope(string line) + { + AppendLine(line); + return BeginScope(); + } + + /// + /// Begins a new scope, incrementing the indent level until the scope is disposed. + /// + /// An representing the scope returned. + public IDisposable BeginScope() + { + Content.Append(new string(' ', IndentLevel)).AppendLine("{"); + IndentLevel += 4; + return _scopeTracker; + } + + /// + /// Ends a scope, decrementing the indent level. + /// + public void EndScope() + { + IndentLevel -= 4; + Content.Append(new string(' ', IndentLevel)).AppendLine("}"); + } + + /// + /// Converts the current code writer to a . + /// + /// A string representing the code written to the code writer. + public override string ToString() + => Content.ToString(); + + /// + /// An implementation of responsible for scope decrementing. + /// + class ScopeTracker : IDisposable + { + /// + /// Gets the That created this . + /// + public CodeWriter Parent { get; } + + /// + /// Constructs a new . + /// + /// The parent that created the . + public ScopeTracker(CodeWriter parent) + { + Parent = parent; + } + + /// + /// Disposes and ends the scope of this . + /// + public void Dispose() + { + Parent.EndScope(); + } + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs b/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs new file mode 100644 index 00000000..8ed422cf --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/ConsoleUtils.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to the console. + /// + internal class ConsoleUtils + { + /// + /// Reads a secret input from STDIN. + /// + /// + /// The entered input received from STDIN. + /// + public static string ReadSecretInput() + { + string input = ""; + ConsoleKey key; + do + { + var keyInfo = Console.ReadKey(true); + key = keyInfo.Key; + + if (key == ConsoleKey.Backspace) + input = input.Length > 0 ? input[..^1] : ""; + else if(!char.IsControl(keyInfo.KeyChar)) + input += keyInfo.KeyChar; + } + while (key != ConsoleKey.Enter); + + return input; + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/FileUtils.cs b/src/EdgeDB.Net.CLI/Utils/FileUtils.cs new file mode 100644 index 00000000..aef28823 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/FileUtils.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to file operations. + /// + internal static class FileUtils + { + /// + /// Waits synchronously for a file to be released. + /// + /// The file path. + /// The timeout. + /// if the file was released; otherwise . + public static bool WaitForHotFile(string path, int timeout = 5000) + { + var start = DateTime.UtcNow; + while (true) + { + try + { + using var fs = File.Open(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + fs.Close(); + return true; + } + catch + { + if ((DateTime.UtcNow - start).TotalMilliseconds >= timeout) + return false; + + Thread.Sleep(200); + } + } + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/HashUtils.cs b/src/EdgeDB.Net.CLI/Utils/HashUtils.cs new file mode 100644 index 00000000..60716046 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/HashUtils.cs @@ -0,0 +1,27 @@ +using EdgeDB.Utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to hashes. + /// + internal static class HashUtils + { + /// + /// Hashes edgeql for an autogenerated file header. + /// + /// The edgeql to hash. + /// The hashed edgeql as hex. + public static string HashEdgeQL(string edgeql) + { + using var algo = SHA256.Create(); + return HexConverter.ToHex(algo.ComputeHash(Encoding.UTF8.GetBytes(edgeql))); + } + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs b/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs new file mode 100644 index 00000000..750e7bd0 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/ProjectUtils.cs @@ -0,0 +1,152 @@ +using CliWrap; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods realted to edgedb projects. + /// + internal static class ProjectUtils + { + /// + /// Gets the edgedb project root from the current directory. + /// + /// The project root directory. + /// The project could not be found. + public static string GetProjectRoot() + { + var directory = Environment.CurrentDirectory; + bool foundRoot = false; + + while (!foundRoot) + { + if ( + !(foundRoot = Directory.GetFiles(directory, "*", SearchOption.TopDirectoryOnly).Any(x => x.EndsWith($"{Path.DirectorySeparatorChar}edgedb.toml"))) && + (directory = Directory.GetParent(directory!)?.FullName) is null) + throw new FileNotFoundException("Could not find edgedb.toml in the current and parent directories"); + } + + return directory; + } + + /// + /// Starts a watcher process. + /// + /// The connection info for the watcher process. + /// The project root directory. + /// The output directory for files the watcher generates to place. + /// The namespace for generated files. + /// The started watcher process id. + public static int StartWatchProcess(EdgeDBConnection connection, string root, string outputDir, string @namespace) + { + var current = Process.GetCurrentProcess(); + var connString = JsonConvert.SerializeObject(connection).Replace("\"", "\\\""); + + return Process.Start(new ProcessStartInfo + { + FileName = current.MainModule!.FileName, + Arguments = $"file-watch-internal --connection \"{connString}\" --dir {root} --output \"{outputDir}\" --namespace \"{@namespace}\"", + UseShellExecute = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + })!.Id; + } + + /// + /// Gets the watcher process for the provided root directory. + /// + /// The project root. + /// + /// The watcher or if not found. + /// + public static Process? GetWatcherProcess(string root) + { + var file = Path.Combine(root, "edgeql.dotnet.watcher.process"); + if (File.Exists(file) && int.TryParse(File.ReadAllText(file), out var id)) + { + try + { + return Process.GetProcesses().FirstOrDefault(x => x.Id == id); + } + catch { return null; } + } + + return null; + } + + /// + /// Registers the current process as the watcher project for the given project root. + /// + /// The project root. + public static void RegisterProcessAsWatcher(string root) + { + var id = Environment.ProcessId; + + File.WriteAllText(Path.Combine(root, "edgeql.dotnet.watcher.process"), $"{id}"); + + // add to gitignore if its here + var gitignore = Path.Combine(root, ".gitignore"); + if (File.Exists(gitignore)) + { + var contents = File.ReadAllText(gitignore); + + if(!contents.Contains("edgeql.dotnet.watcher.process")) + { + contents += $"{Environment.NewLine}# EdgeDB.Net CLI watcher info file{Environment.NewLine}edgeql.dotnet.watcher.process"; + File.WriteAllText(gitignore, contents); + } + } + } + + /// + /// Creates a dotnet project. + /// + /// The target directory. + /// The name of the project + /// The project failed to be created. + public static async Task CreateGeneratedProjectAsync(string root, string name) + { + var result = await Cli.Wrap("dotnet") + .WithArguments($"new classlib --framework \"net6.0\" -n {name}") + .WithWorkingDirectory(root) + .WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError())) + .WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput())) + .ExecuteAsync(); + + if (result.ExitCode != 0) + throw new IOException($"Failed to create new project"); + + result = await Cli.Wrap("dotnet") + .WithArguments("add package EdgeDB.Net.Driver") + .WithWorkingDirectory(Path.Combine(root, name)) + .WithStandardErrorPipe(PipeTarget.ToStream(Console.OpenStandardError())) + .WithStandardOutputPipe(PipeTarget.ToStream(Console.OpenStandardOutput())) + .ExecuteAsync(); + + if (result.ExitCode != 0) + throw new IOException($"Failed to create new project"); + + // remove default file + File.Delete(Path.Combine(root, name, "Class1.cs")); + } + + /// + /// Gets a list of edgeql file paths for the provided root directory. + /// + /// + /// migration files are ignored. + /// + /// The root directory to scan for edgeql files. + /// + /// An that enumerates a collection of files ending in .edgeql. + /// + public static IEnumerable GetTargetEdgeQLFiles(string root) + => Directory.GetFiles(root, "*.edgeql", SearchOption.AllDirectories).Where(x => !x.StartsWith(Path.Combine(root, "dbschema", "migrations"))); + } +} diff --git a/src/EdgeDB.Net.CLI/Utils/TextUtils.cs b/src/EdgeDB.Net.CLI/Utils/TextUtils.cs new file mode 100644 index 00000000..61e2eb69 --- /dev/null +++ b/src/EdgeDB.Net.CLI/Utils/TextUtils.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace EdgeDB.CLI.Utils +{ + /// + /// A utility class containing methods related to text operations. + /// + internal static class TextUtils + { + /// + /// The current culture info. + /// + private static CultureInfo? _cultureInfo; + + /// + /// Converts the given string to pascal case. + /// + /// The string to convert to pascal case. + /// A pascal-cased version of the input string. + public static string ToPascalCase(string input) + { + _cultureInfo ??= CultureInfo.CurrentCulture; + var t = Regex.Replace(input, @"[^^]([A-Z])", m => $"{m.Value[0]} {m.Groups[1].Value}"); + + return _cultureInfo.TextInfo.ToTitleCase(t.Replace("_", " ")).Replace(" ", ""); + } + + /// + /// Converts the given string to camel case. + /// + /// The string to convert to pascal case. + /// A camel-cased version of the input string. + public static string ToCamelCase(string input) + { + var p = ToPascalCase(input); + return $"{p[0].ToString().ToLower()}{p[1..]}"; + } + } +} diff --git a/src/EdgeDB.Net.Driver/AssemblyInfo.cs b/src/EdgeDB.Net.Driver/AssemblyInfo.cs index 2c4eae45..c2b2a32a 100644 --- a/src/EdgeDB.Net.Driver/AssemblyInfo.cs +++ b/src/EdgeDB.Net.Driver/AssemblyInfo.cs @@ -1,6 +1,7 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("EdgeDB.Net.QueryBuilder")] +[assembly: InternalsVisibleTo("EdgeDB.Net.CLI")] [assembly: InternalsVisibleTo("EdgeDB.Runtime")] [assembly: InternalsVisibleTo("EdgeDB.ExampleApp")] [assembly: InternalsVisibleTo("EdgeDB.DotnetTool")] diff --git a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs index 08526489..a0f87843 100644 --- a/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs +++ b/src/EdgeDB.Net.Driver/Clients/EdgeDBBinaryClient.cs @@ -154,6 +154,25 @@ public RawExecuteResult(ICodec codec, List data) } } + internal readonly struct ParseResult + { + public readonly CodecInfo InCodec; + public readonly CodecInfo OutCodec; + public readonly IDictionary State; + public readonly Cardinality Cardinality; + public readonly Capabilities Capabilities; + + public ParseResult(CodecInfo inCodec, CodecInfo outCodec, IDictionary state, + Cardinality cardinality, Capabilities capabilities) + { + InCodec = inCodec; + OutCodec = outCodec; + State = state; + Cardinality = cardinality; + Capabilities = capabilities; + } + } + /// A general error occored. /// The client received an . /// The client received an unexpected message. @@ -180,73 +199,10 @@ internal async Task ExecuteInternalAsync(string query, IDictio try { - var cacheKey = CodecBuilder.GetCacheHashKey(query, cardinality ?? Cardinality.Many, format); - - var serializedState = Session.Serialize(); + var parsed = await ParseAsync(query, cardinality ?? Cardinality.Many, format, capabilities, token).ConfigureAwait(false); - List p = new(); - - if (!CodecBuilder.TryGetCodecs(cacheKey, out var inCodecInfo, out var outCodecInfo)) - { - bool parseHandlerPredicate(IReceiveable? packet) - { - p.Add(packet); - switch (packet) - { - case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.StateMismatchError: - throw new EdgeDBErrorException(err); - case CommandDataDescription descriptor: - { - outCodecInfo = new(descriptor.OutputTypeDescriptorId, - CodecBuilder.BuildCodec(descriptor.OutputTypeDescriptorId, descriptor.OutputTypeDescriptorBuffer)); - - inCodecInfo = new(descriptor.InputTypeDescriptorId, - CodecBuilder.BuildCodec(descriptor.InputTypeDescriptorId, descriptor.InputTypeDescriptorBuffer)); - - CodecBuilder.UpdateKeyMap(cacheKey, descriptor.InputTypeDescriptorId, descriptor.OutputTypeDescriptorId); - } - break; - case StateDataDescription stateDescriptor: - { - _stateCodec = CodecBuilder.BuildCodec(stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); - _stateDescriptorId = stateDescriptor.TypeDescriptorId; - } - break; - case ReadyForCommand ready: - TransactionState = ready.TransactionState; - return true; - default: - break; - } - - return false; - } - - var stateBuf = _stateCodec?.Serialize(serializedState)!; - - var result = await Duplexer.DuplexAndSyncAsync(new Parse - { - Capabilities = capabilities, - Query = query, - Format = format, - ExpectedCardinality = cardinality ?? Cardinality.Many, - ExplicitObjectIds = _config.ExplicitObjectIds, - StateTypeDescriptorId = _stateDescriptorId, - StateData = stateBuf, - ImplicitLimit = _config.ImplicitLimit, - ImplicitTypeNames = true, // used for type builder - ImplicitTypeIds = true, // used for type builder - }, parseHandlerPredicate, alwaysReturnError: false, token: token).ConfigureAwait(false); - - if (outCodecInfo is null) - throw new MissingCodecException("Couldn't find a valid output codec"); - - if (inCodecInfo is null) - throw new MissingCodecException("Couldn't find a valid input codec"); - } - - if (inCodecInfo.Codec is not IArgumentCodec argumentCodec) - throw new MissingCodecException($"Cannot encode arguments, {inCodecInfo.Codec} is not a registered argument codec"); + if (parsed.InCodec.Codec is not IArgumentCodec argumentCodec) + throw new MissingCodecException($"Cannot encode arguments, {parsed.InCodec.Codec} is not a registered argument codec"); List receivedData = new(); @@ -275,20 +231,20 @@ bool handler(IReceiveable msg) ExpectedCardinality = cardinality ?? Cardinality.Many, ExplicitObjectIds = _config.ExplicitObjectIds, StateTypeDescriptorId = _stateDescriptorId, - StateData = _stateCodec?.Serialize(serializedState), + StateData = _stateCodec?.Serialize(parsed.State), ImplicitTypeNames = true, // used for type builder ImplicitTypeIds = true, // used for type builder Arguments = argumentCodec?.SerializeArguments(args) , ImplicitLimit = _config.ImplicitLimit, - InputTypeDescriptorId = inCodecInfo.Id, - OutputTypeDescriptorId = outCodecInfo.Id, + InputTypeDescriptorId = parsed.InCodec.Id, + OutputTypeDescriptorId = parsed.OutCodec.Id, }, handler, alwaysReturnError: false, token: linkedToken).ConfigureAwait(false); executeResult.ThrowIfErrrorResponse(); execResult = new ExecuteResult(true, null, null, query); - return new RawExecuteResult(outCodecInfo.Codec!, receivedData); + return new RawExecuteResult(parsed.OutCodec.Codec!, receivedData); } catch (OperationCanceledException) { @@ -330,6 +286,79 @@ bool handler(IReceiveable msg) } } + internal async Task ParseAsync(string query, Cardinality cardinality, IOFormat format, Capabilities? capabilities, CancellationToken token) + { + var cacheKey = CodecBuilder.GetCacheHashKey(query, cardinality, format); + + var serializedState = Session.Serialize(); + + List p = new(); + + if (!CodecBuilder.TryGetCodecs(cacheKey, out var inCodecInfo, out var outCodecInfo)) + { + bool parseHandlerPredicate(IReceiveable? packet) + { + p.Add(packet); + switch (packet) + { + case ErrorResponse err when err.ErrorCode is not ServerErrorCodes.StateMismatchError: + throw new EdgeDBErrorException(err); + case CommandDataDescription descriptor: + { + outCodecInfo = new(descriptor.OutputTypeDescriptorId, + CodecBuilder.BuildCodec(descriptor.OutputTypeDescriptorId, descriptor.OutputTypeDescriptorBuffer)); + + inCodecInfo = new(descriptor.InputTypeDescriptorId, + CodecBuilder.BuildCodec(descriptor.InputTypeDescriptorId, descriptor.InputTypeDescriptorBuffer)); + + CodecBuilder.UpdateKeyMap(cacheKey, descriptor.InputTypeDescriptorId, descriptor.OutputTypeDescriptorId); + + cardinality = descriptor.Cardinality; + capabilities = descriptor.Capabilities; + } + break; + case StateDataDescription stateDescriptor: + { + _stateCodec = CodecBuilder.BuildCodec(stateDescriptor.TypeDescriptorId, stateDescriptor.TypeDescriptorBuffer); + _stateDescriptorId = stateDescriptor.TypeDescriptorId; + } + break; + case ReadyForCommand ready: + TransactionState = ready.TransactionState; + return true; + default: + break; + } + + return false; + } + + var stateBuf = _stateCodec?.Serialize(serializedState)!; + + var result = await Duplexer.DuplexAndSyncAsync(new Parse + { + Capabilities = capabilities, + Query = query, + Format = format, + ExpectedCardinality = cardinality, + ExplicitObjectIds = _config.ExplicitObjectIds, + StateTypeDescriptorId = _stateDescriptorId, + StateData = stateBuf, + ImplicitLimit = _config.ImplicitLimit, + ImplicitTypeNames = true, // used for type builder + ImplicitTypeIds = true, // used for type builder + }, parseHandlerPredicate, alwaysReturnError: false, token: token).ConfigureAwait(false); + + if (outCodecInfo is null) + throw new MissingCodecException("Couldn't find a valid output codec"); + + if (inCodecInfo is null) + throw new MissingCodecException("Couldn't find a valid input codec"); + } + + return new ParseResult(inCodecInfo, outCodecInfo, serializedState, cardinality, capabilities ?? Capabilities.ReadOnly); + } + /// /// A general error occored. /// The client received an . @@ -664,7 +693,10 @@ private void ParseServerSettings(ParameterStatus status) /// public override async ValueTask ConnectAsync(CancellationToken token = default) { - await _connectSemaphone.WaitAsync(); + if (IsConnected) + return; + + await _connectSemaphone.WaitAsync(token); try { diff --git a/src/EdgeDB.Net.Driver/Codecs/Object.cs b/src/EdgeDB.Net.Driver/Codecs/Object.cs index 28b05ec0..6f32d170 100644 --- a/src/EdgeDB.Net.Driver/Codecs/Object.cs +++ b/src/EdgeDB.Net.Driver/Codecs/Object.cs @@ -4,22 +4,22 @@ namespace EdgeDB.Codecs { internal class Object : ICodec, IArgumentCodec { - private readonly ICodec[] _innerCodecs; - private readonly string[] _propertyNames; + internal readonly ICodec[] InnerCodecs; + internal readonly string[] PropertyNames; internal Object(ICodec[] innerCodecs, string[] propertyNames) { - _innerCodecs = innerCodecs; - _propertyNames = propertyNames; + InnerCodecs = innerCodecs; + PropertyNames = propertyNames; } public object? Deserialize(ref PacketReader reader) { var numElements = reader.ReadInt32(); - if (_innerCodecs.Length != numElements) + if (InnerCodecs.Length != numElements) { - throw new ArgumentException($"codecs mismatch for tuple: expected {numElements} codecs, got {_innerCodecs.Length} codecs"); + throw new ArgumentException($"codecs mismatch for tuple: expected {numElements} codecs, got {InnerCodecs.Length} codecs"); } dynamic data = new ExpandoObject(); @@ -29,7 +29,7 @@ internal Object(ICodec[] innerCodecs, string[] propertyNames) { // reserved reader.Skip(4); - var name = _propertyNames[i]; + var name = PropertyNames[i]; var length = reader.ReadInt32(); if(length is -1) @@ -42,7 +42,7 @@ internal Object(ICodec[] innerCodecs, string[] propertyNames) object? value; - value = _innerCodecs[i].Deserialize(innerData); + value = InnerCodecs[i].Deserialize(innerData); dataDictionary.Add(name, value); } @@ -60,7 +60,7 @@ public void SerializeArguments(PacketWriter writer, object? value) object?[]? values = null; if (value is IDictionary dict) - values = _propertyNames.Select(x => dict[x]).ToArray(); + values = PropertyNames.Select(x => dict[x]).ToArray(); else if (value is object?[] arr) value = arr; @@ -73,7 +73,7 @@ public void SerializeArguments(PacketWriter writer, object? value) for (int i = 0; i != values.Length; i++) { var element = values[i]; - var innerCodec = _innerCodecs[i]; + var innerCodec = InnerCodecs[i]; // reserved innerWriter.Write(0); diff --git a/src/EdgeDB.Net.Driver/Codecs/Tuple.cs b/src/EdgeDB.Net.Driver/Codecs/Tuple.cs index feb33fd0..468d3962 100644 --- a/src/EdgeDB.Net.Driver/Codecs/Tuple.cs +++ b/src/EdgeDB.Net.Driver/Codecs/Tuple.cs @@ -5,20 +5,20 @@ namespace EdgeDB.Codecs { internal class Tuple : ICodec { - private readonly ICodec[] _innerCodecs; + internal readonly ICodec[] InnerCodecs; public Tuple(ICodec[] innerCodecs) { - _innerCodecs = innerCodecs; + InnerCodecs = innerCodecs; } public TransientTuple Deserialize(ref PacketReader reader) { var numElements = reader.ReadInt32(); - if(_innerCodecs.Length != numElements) + if(InnerCodecs.Length != numElements) { - throw new ArgumentException($"codecs mismatch for tuple: expected {numElements} codecs, got {_innerCodecs.Length} codecs"); + throw new ArgumentException($"codecs mismatch for tuple: expected {numElements} codecs, got {InnerCodecs.Length} codecs"); } // deserialize our values @@ -41,10 +41,10 @@ public TransientTuple Deserialize(ref PacketReader reader) reader.ReadBytes(length, out var data); var innerReader = new PacketReader(data); - values[i] = _innerCodecs[i].Deserialize(ref innerReader); + values[i] = InnerCodecs[i].Deserialize(ref innerReader); } - return new TransientTuple(_innerCodecs.Select(x => x.ConverterType).ToArray(), values); + return new TransientTuple(InnerCodecs.Select(x => x.ConverterType).ToArray(), values); } public void Serialize(PacketWriter writer, TransientTuple value) diff --git a/src/EdgeDB.Net.Driver/Serializer/SchemaTypeBuilders/TypeBuilder.cs b/src/EdgeDB.Net.Driver/Serializer/SchemaTypeBuilders/TypeBuilder.cs index f17345a6..7d9c6410 100644 --- a/src/EdgeDB.Net.Driver/Serializer/SchemaTypeBuilders/TypeBuilder.cs +++ b/src/EdgeDB.Net.Driver/Serializer/SchemaTypeBuilders/TypeBuilder.cs @@ -127,7 +127,7 @@ internal static bool IsValidObjectType(Type type) ?.GetCustomAttribute() != null; // allow abstract passthru - return type.IsAbstract ? true : (type.IsClass || type.IsValueType) && !type.IsSealed && validConstructor; + return type.IsAbstract ? true : (type.IsClass || type.IsValueType) && validConstructor; } internal static bool TryGetCollectionParser(Type type, out Func? builder) diff --git a/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs b/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs index 708500aa..ba8e5746 100644 --- a/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs +++ b/src/EdgeDB.Net.Driver/Utils/ReflectionUtils.cs @@ -25,6 +25,12 @@ public static bool IsSubclassOfRawGeneric(Type generic, Type? toCheck) return false; } + public static bool IsSubclassOfInterfaceGeneric(Type generic, Type? toCheck) + { + var interfaces = toCheck!.GetInterfaces(); + return interfaces.Any(x => IsSubclassOfRawGeneric(generic, x)); + } + public static bool TryGetRawGeneric(Type generic, Type? toCheck, out Type? genericReference) { genericReference = null; diff --git a/tools/EdgeDB.DotnetTool/Commands/Generate.cs b/tools/EdgeDB.DotnetTool/Commands/Generate.cs deleted file mode 100644 index 33d161d2..00000000 --- a/tools/EdgeDB.DotnetTool/Commands/Generate.cs +++ /dev/null @@ -1,117 +0,0 @@ -using CommandLine; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using YamlDotNet.Serialization; -using YamlDotNet.Serialization.NamingConventions; - -namespace EdgeDB.DotnetTool.Commands -{ - [Verb("generate", HelpText = "Generates classes based off of a schema")] - internal class Generate : ICommand - { - [Option('c', "dsn", HelpText = "The DSN connection string to connect to the remote instance", Required = false)] - public string? ConnectionString { get; set; } - - [Option('f', "file", Required = false, HelpText = "The file location of the schema")] - public string? FilePath { get; set; } - - [Option('n', "namespace", Required = false, HelpText = "The namespace for the generated code files", Default = "EdgeDB.Generated")] - public string? Namespace { get; set; } - - [Option('o', "output", Required = false, HelpText = "The output directory for the generated files", Default = "./Generated")] - public string? OutputDir { get; set; } - - [Option('v', "verbose", HelpText = "Enables verbose output")] - public bool Verbose { get; set; } - - public void Execute() - { - // use either file or connection - string? schema = null; - - if (FilePath != null) - schema = File.ReadAllText(FilePath!); - else if (ConnectionString != null) - { - Task.Run(() => - { - //var client = new EdgeDBTcpClient(EdgeDBConnection.FromDSN(ConnectionString), new EdgeDBConfig - //{ - // // TODO: config? - //}); - - //await client.ConnectAsync(); - //var result = await client.QuerySingleAsync($"describe schema as sdl"); - return Task.CompletedTask; - - }).GetAwaiter().GetResult(); - } - - if(schema == null) - { - Console.Error.WriteLine("Please either specify the schema file (-n) or a connection string (-c)"); - return; - } - - Console.WriteLine("Generating module..."); - List modules; - var serializer = new SerializerBuilder() - .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull | DefaultValuesHandling.OmitEmptyCollections | DefaultValuesHandling.OmitDefaults) - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .Build(); - - try - { - modules = new SchemaReader(schema).Read(); - Console.WriteLine("Parsed schema!"); - if(Verbose) - Console.WriteLine(serializer.Serialize(modules)); - } - catch(Exception x) - { - Console.Error.WriteLine($"Failed to read schema: {x}"); - return; - } - - try - { - var builder = new ClassBuilder(OutputDir!, Namespace!); - foreach (var module in modules) - { - Console.WriteLine($"Generating {module.Name}..."); - builder.Generate(module, GetValidDotnetName); - } - - } - catch(Exception x) - { - Console.Error.WriteLine($"Failed to build classes from schema: {x}"); - return; - } - - Console.WriteLine("Generation succeeded"); - } - - private static string GetValidDotnetName(string str) - { - Console.WriteLine($"The name \"{str}\" isn't a valid name in DotNet, what would you want to name it instead?"); - Console.Write("> "); - - while (true) - { - var newName = Console.ReadLine(); - - if (newName == null) - continue; - - if (ClassBuilder.IsValidDotnetName(newName)) - { - return newName; - } - } - } - } -} diff --git a/tools/EdgeDB.DotnetTool/EdgeDB.DotnetTool.csproj b/tools/EdgeDB.DotnetTool/EdgeDB.DotnetTool.csproj deleted file mode 100644 index 897e6f52..00000000 --- a/tools/EdgeDB.DotnetTool/EdgeDB.DotnetTool.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Exe - net6.0 - enable - enable - - - - True - 5 - - - - True - 5 - - - - - - - - - - - - - diff --git a/tools/EdgeDB.DotnetTool/ICommand.cs b/tools/EdgeDB.DotnetTool/ICommand.cs deleted file mode 100644 index 909ee4c7..00000000 --- a/tools/EdgeDB.DotnetTool/ICommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal interface ICommand - { - void Execute(); - } -} diff --git a/tools/EdgeDB.DotnetTool/Lexer/SchemaBuffer.cs b/tools/EdgeDB.DotnetTool/Lexer/SchemaBuffer.cs deleted file mode 100644 index dc1d8e36..00000000 --- a/tools/EdgeDB.DotnetTool/Lexer/SchemaBuffer.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool.Lexer -{ - internal class SchemaBuffer - { - readonly string _buffer; - int _bufferPos; - - public int Column { get; private set; } - public int Line { get; private set; } - - public SchemaBuffer(string schema) - { - _buffer = schema; - _bufferPos = 0; - Column = 1; - Line = 1; - - } - - public string Peek(int count) - { - string ret = ""; - while (count > 0) - { - ret += GetNextTextElement(ret.Length); - count--; - } - return ret; - } - - public string Read(int count) - { - return MovePosition(Peek(count)); - } - - public string PeekUntil(string str) - { - string ret = ""; - while (!ret.Contains(str)) - { - string temp = GetNextTextElement(ret.Length); - if (temp.Length == 0) - { - break; - } - ret += temp; - } - return ret; - } - - public string ReadUntil(string str) - { - return MovePosition(PeekUntil(str)); - } - - public string ReadWhile(Predicate pred) - { - string ret = ""; - while (true) - { - string temp = GetNextTextElement(ret.Length); - if (temp.Length == 0 || !pred(temp)) - { - break; - } - ret += temp; - } - return MovePosition(ret); - } - - private string MovePosition(string portion) - { - _bufferPos += portion.Length; - int index = portion.LastIndexOf('\n'); - if (index >= 0) - { - Column = new StringInfo(portion[(index + 1)..]).LengthInTextElements + 1; - Line += portion.Count((ch) => ch == '\n'); - } - else - { - Column += new StringInfo(portion).LengthInTextElements; - } - return portion; - } - - private string GetNextTextElement(int offset) - { - while (true) - { - string elem = StringInfo.GetNextTextElement(_buffer, _bufferPos + offset); - if (_bufferPos + offset + elem.Length < _buffer.Length) - { - return elem; - } - return elem; - } - } - - } -} diff --git a/tools/EdgeDB.DotnetTool/Lexer/SchemaLexer.cs b/tools/EdgeDB.DotnetTool/Lexer/SchemaLexer.cs deleted file mode 100644 index 6cf39bd6..00000000 --- a/tools/EdgeDB.DotnetTool/Lexer/SchemaLexer.cs +++ /dev/null @@ -1,252 +0,0 @@ -namespace EdgeDB.DotnetTool.Lexer -{ - internal class SchemaLexer - { - private readonly Dictionary _tokens = new() - { - { "module", TokenType.Module }, - { "{", TokenType.BeginBrace }, - { "}", TokenType.EndBrace }, - { "required", TokenType.Required }, - { "property", TokenType.Property }, - { "constraint", TokenType.Constraint }, - { "->", TokenType.TypeArrow }, - { "multi", TokenType.Multi }, - { "single", TokenType.Single }, - { "link", TokenType.Link }, - { "abstract", TokenType.Abstract }, - { ";", TokenType.Semicolon }, - { ":=", TokenType.Assignment }, - { "on", TokenType.On }, - { "extending", TokenType.Extending }, - { ",", TokenType.Comma }, - { "(", TokenType.BeginParenthesis }, - { ")", TokenType.EndParenthesis }, - { "annotation", TokenType.Annotation }, - { "type", TokenType.Type }, - { "index", TokenType.Index }, - { "scalar", TokenType.Scalar }, - { "function", TokenType.Function }, - }; - - private readonly SchemaBuffer _reader; - private readonly Stack _stack; - private Token? _previousToken; - - public SchemaLexer(string schema) - { - _reader = new SchemaBuffer(schema); - _stack = new(); - } - - public Token PeekToken() - { - if (_stack.Count == 0) - _stack.Push(ReadTokenInternal()); - - return _stack.Peek(); - } - - public Token ReadToken() - { - if (_stack.Count > 0) - return _stack.Pop(); - return ReadTokenInternal(); - } - - private Token ReadTokenInternal() - { - ReadWhitespace(); - - var elArr = _tokens.Select(x => x.Key.Length).Distinct(); - - foreach (var item in elArr) - { - if (_tokens.TryGetValue(_reader.Peek(item), out var token)) - { - _reader.Read(item); - - switch (token) - { - case TokenType.Assignment - or TokenType.TypeArrow - or TokenType.Extending - or TokenType.Constraint when _previousToken?.Type != TokenType.Abstract: - { - ReadWhitespace(); - List delimiters = new(new char[] { ';', '{' }); - if (token == TokenType.Extending && _reader.Peek(4) != "enum") - delimiters.Add(','); - - var value = ReadValue(token == TokenType.Assignment, delimiters.ToArray()); - return NewToken(token, value); - } - - case TokenType.Module - or TokenType.Type - or TokenType.Property - or TokenType.Link: - { - ReadWhitespace(); - var value = ReadValue(false, ';'); - return NewToken(token, value); - } - - case TokenType.Constraint when _previousToken?.Type == TokenType.Abstract: - { - // read value until the end of line or until ; - var value = ReadValue(true, ';'); - // read the semi colon - if (_reader.Peek(1) == ";") - value += _reader.Read(1); - return NewToken(TokenType.Constraint, value); - } - case TokenType.Function: - { - // read the value until the closing block - //var value = ReadValue(true, '{'); - - // read the name - var func = _reader.ReadUntil("("); - var funcParamsDepth = 1; - func += _reader.ReadWhile(x => - { - if (x == "(") - funcParamsDepth++; - - if (x == ")") - funcParamsDepth--; - - return funcParamsDepth > 0; - }); - func += _reader.Read(1); // the closing prarms brace - - // type arrow - func += ReadWhitespace(); - func += _reader.Read(2); - func += ReadWhitespace(); - // return type - func += ReadValue(); - func += ReadWhitespace(); - // check if its a bracket function - if (_reader.Peek(1) == "{") - { - var depth = 1; - _reader.Read(1); - func += _reader.ReadWhile(x => - { - if (x == "{") - depth++; - if (x == "}") - depth--; - - return depth > 0; - }); - - func += _reader.Read(1); - - // add semi colon - if (_reader.Peek(1) == ";") - func += _reader.Read(1); - - return NewToken(TokenType.Function, func); - } - else if (_reader.Peek(5) == "using") - { - // assume its a parentheses block? - func += _reader.Read(5); // read using - func += ReadWhitespace(); // read whitespace - func += _reader.Read(1); // read starting parantheses - var depth = 1; - func += _reader.ReadWhile(x => - { - if (x == "(") - depth++; - if (x == ")") - depth--; - - return depth > 0; - }); - - func += _reader.Read(1); // read closing parantheses - - // add semi colon - if (_reader.Peek(1) == ";") - func += _reader.Read(1); - - return NewToken(TokenType.Function, func); - } - - var a = _reader.Read(5); - - return NewToken(TokenType.Function, ""); - } - default: - return NewToken(token, null); - - } - - } - } - - var st = _reader.Peek(1); - - if (st == null || st.Length == 0) - { - // end of file - return NewToken(TokenType.EndOfFile); - } - - var c = _reader.Peek(1)[0]; - - // identifier - var val = ReadValue(false, ';'); - - return NewToken(TokenType.Identifier, val); - } - - private string ReadValue(bool ignoreSpace = false, params char[] delimiters) - { - // read untill we get to a quote, then read everything in the quote until whitespace - bool isEscaped = false; - return _reader.ReadWhile(x => - { - if (x is "'" or "\"" or "[" or "]" or "`" or "<" or ">") - isEscaped = !isEscaped; - - return isEscaped || ((ignoreSpace || !char.IsWhiteSpace(x, 0)) && (delimiters.Length == 0 || !delimiters.Contains(x[0]))); - }); - } - - public Token Expect(TokenType type) - { - var t = PeekToken(); - - if (type == TokenType.EndOfFile) - { - throw new EndOfStreamException("Unexpected end of file"); - } - - if (t.Type != type) - throw new ArgumentException($"Unexpected token! Expected {type} but got {t.Type} at {t.StartLine}:{t.StartPos}", nameof(type)); - - return ReadToken(); - } - - private Token NewToken(TokenType t, string? value = null) - { - var token = new Token - { - Type = t, - Value = value, - StartPos = _reader.Column, - StartLine = _reader.Line - }; - _previousToken = token; - return token; - } - - private string ReadWhitespace() - => _reader.ReadWhile(x => char.IsWhiteSpace(x, 0)); - } -} diff --git a/tools/EdgeDB.DotnetTool/Lexer/Token.cs b/tools/EdgeDB.DotnetTool/Lexer/Token.cs deleted file mode 100644 index 1e073f40..00000000 --- a/tools/EdgeDB.DotnetTool/Lexer/Token.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool.Lexer -{ - internal class Token - { - public TokenType Type { get; set; } - - public string? Value { get; set; } - - public int StartPos { get; set; } - - public int StartLine { get; set; } - - public int EndPos { get; set; } - - public int EndLine { get; set; } - } -} diff --git a/tools/EdgeDB.DotnetTool/Lexer/TokenType.cs b/tools/EdgeDB.DotnetTool/Lexer/TokenType.cs deleted file mode 100644 index 9812a551..00000000 --- a/tools/EdgeDB.DotnetTool/Lexer/TokenType.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool.Lexer -{ - internal enum TokenType - { - BeginBrace, - EndBrace, - Required, - Property, - Constraint, - TypeArrow, - Multi, - Link, - Abstract, - Semicolon, - Assignment, - Single, - On, - Extending, - Comma, - BeginParenthesis, - EndParenthesis, - Annotation, - Module, - Type, - Index, - Scalar, - Function, - - Identifier, - EndOfFile - } -} diff --git a/tools/EdgeDB.DotnetTool/Program.cs b/tools/EdgeDB.DotnetTool/Program.cs deleted file mode 100644 index 2d438436..00000000 --- a/tools/EdgeDB.DotnetTool/Program.cs +++ /dev/null @@ -1,7 +0,0 @@ -using CommandLine; -using EdgeDB.DotnetTool; -using System.Reflection; - -var commands = typeof(Program).Assembly.GetTypes().Where(x => x.GetInterfaces().Any(x => x == typeof(ICommand))); - -Parser.Default.ParseArguments(args, commands.ToArray()).WithParsed(t => t.Execute()); \ No newline at end of file diff --git a/tools/EdgeDB.DotnetTool/Properties/launchSettings.json b/tools/EdgeDB.DotnetTool/Properties/launchSettings.json deleted file mode 100644 index 27dbf5d1..00000000 --- a/tools/EdgeDB.DotnetTool/Properties/launchSettings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "profiles": { - "EdgeDB.DotnetTool": { - "commandName": "Project", - "commandLineArgs": "generate -f C:\\Users\\lynch\\source\\repos\\EdgeDB\\src\\EdgeDB.DotnetTool\\test.esdl -v" - }, - "Profile 1": { - "commandName": "Executable" - } - } -} \ No newline at end of file diff --git a/tools/EdgeDB.DotnetTool/Schemas/ClassBuilder.cs b/tools/EdgeDB.DotnetTool/Schemas/ClassBuilder.cs deleted file mode 100644 index 81dfe3f7..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/ClassBuilder.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Globalization; -using System.Text.RegularExpressions; - -namespace EdgeDB.DotnetTool -{ - internal class ClassBuilder - { - private readonly string _outputDir; - private readonly string _generatedNamespace; - - private readonly TextInfo _textInfo = new CultureInfo("en-US", false).TextInfo; - - - public ClassBuilder(string outputDir, string generatedNamespace) - { - _outputDir = outputDir; - _generatedNamespace = generatedNamespace; - } - - public void Generate(Module module, Func nameCallback) - { - var context = new ClassBuilderContext - { - Module = module, - NameCallback = nameCallback - }; - - // create the module folder - var folder = Path.Combine(_outputDir, _textInfo.ToTitleCase(module.Name!)); - Directory.CreateDirectory(folder); - - context.OutputDir = folder; - - foreach (var type in module.Types) - { - if (context.BuiltTypes.Contains(type)) - continue; - - context.Type = type; - GenerateType(type, folder, context); - - context.BuiltTypes.Add(type); - } - } - - public void GenerateType(Type t, string dir, ClassBuilderContext context) - { - var writer = new CodeWriter(); - - writer.AppendLine($"// Generated on {DateTimeOffset.UtcNow:O}"); - writer.AppendLine("using EdgeDB;"); - - string? name = t.Name; - - if (name != null && Regex.IsMatch(name, @"^[`'](.*?)[`']$")) - name = Regex.Match(name, @"^[`'](.*?)[`']$").Groups[1].Value; - - using (var _ = writer.BeginScope($"namespace {_generatedNamespace}")) - { - name = !IsValidDotnetName(name) - ? context.NameCallback($"{context.Module?.Name}::{name ?? "null"}") - : PascalUtils.ToPascalCase(name); - - t.BuiltName = name; - - if (IsEnum(t)) - { - writer.AppendLine("[EnumSerializer(SerializationMethod.Lower)]"); - using (var __ = writer.BeginScope($"public enum {name}")) - { - // extract values - foreach (Match match in Regex.Matches(t.Extending!, @"(?><| )(.*?)(?>,|>)")) - { - writer.AppendLine($"{match.Groups[1].Value},"); - } - } - } - else - { - // generate class - writer.AppendLine($"[EdgeDBType(\"{t.Name}\")]"); - using (var __ = writer.BeginScope($"public{(t.IsAbstract ? " abstract" : "")} class {name} : {ResolveTypename(context!, t.Extending) ?? "BaseObject"}")) - { - foreach (var prop in t.Properties) - { - if (context.BuildProperties.Contains(prop)) - continue; - - context.Property = prop; - GenerateProperty(writer, prop, context); - - context.BuildProperties.Add(prop); - } - } - } - } - - File.WriteAllText(Path.Combine(dir, $"{name}.g.cs"), writer.ToString()); - Console.WriteLine($"Wrote {name}.g.cs : {Path.Combine(dir, $"{name}.g.cs")}"); - } - - private string? GenerateProperty(CodeWriter writer, Property prop, ClassBuilderContext context) - { - // TODO: build a tree of contraints to use when applying attributes to props. - if (prop.IsStrictlyConstraint) - return null; - - // get the C# type if its a std type - var resolved = ResolveTypename(context, prop.Type); - var type = ResolveScalar(resolved)?.FullName ?? resolved; - - // convert to pascal case for name - var name = prop.Name; - - name = !IsValidDotnetName(name) - ? context.NameCallback($"{context.Module?.Name}::{context.Type?.Name}.{name ?? "null"}") - : PascalUtils.ToPascalCase(name); - - prop.BuiltName = name; - - if (type == null && prop.IsComputed && prop.ComputedValue != null) - { - var computed = prop.ComputedValue; - - var wrappedMatch = Regex.Match(computed, @"^\((.*?)\)$"); - - if (wrappedMatch.Success) - computed = wrappedMatch.Groups[1].Value; - - // check for backlink - if (computed.StartsWith(".<")) - { - // set the cardinaliry to multi since its a backlinl - prop.Cardinality = PropertyCardinality.Multi; - - var match = Regex.Match(computed, @"\[is (.+?)\]"); - - type = match.Success ? ResolveTypename(context, match.Groups[1].Value) : "BaseObject"; - } - else - { - // do a reverse lookup on the root function to see if we can decipher the type - computed = Regex.Replace(computed, @"^.+?::", _ => ""); - var returnType = QueryBuilder.ReverseLookupFunction(computed); - - if (returnType != null) - type = returnType.FullName; - else if (computed.StartsWith(".")) - { - // its a prop ref, generate it - var pName = Regex.Match(computed, @"^\.(\w+)").Groups[1].Value; - var p = context.Type?.Properties.FirstOrDefault(x => x.Name == pName)!; - - if (context.BuildProperties.Any(x => x.Name == p.Name)) - type = context.BuildProperties.FirstOrDefault(x => x.Name == p.Name)!.Type; - else - { - type = GenerateProperty(writer, p, context); - context.BuildProperties.Add(p); - } - } - else - { - type = "object"; - } - } - } - - if (prop.Cardinality == PropertyCardinality.Multi) - type = $"Set<{type}>"; - - writer.AppendLine($"[EdgeDBProperty(\"{prop.Name}\", IsLink = {Lower(prop.IsLink)}, IsRequired = {Lower(prop.Required)}, IsReadOnly = {Lower(prop.ReadOnly)}, IsComputed = {Lower(prop.IsComputed)})]"); - - // TODO: maybe remove set operator for readonly / computed? - writer.AppendLine($"public {type} {name} {{ get; set; }}"); - - prop.Type = type; - - return type; - - } - - public static System.Type? ResolveScalar(string? t) - => t == null - ? null - : PacketSerializer.GetDotnetType(Regex.Replace(t, @".+?::", m => "")); - - private string? ResolveTypename(ClassBuilderContext context, string? name) - { - if (name == null) - return null; - - if (name.StartsWith($"{context.Module!.Name}::")) - name = name[(context.Module.Name!.Length + 2)..]; - - // check our built types - if (context.BuiltTypes.Any(x => x.Name == name)) - { - return context.BuiltTypes.FirstOrDefault(x => x.Name == name)!.BuiltName; - } - else if (context.Module.Types.Any(x => x.Name == name)) // try building it if it has a name ref - { - var type = context.Module.Types.FirstOrDefault(x => x.Name == name)!; - GenerateType(type, context.OutputDir!, context); - context.BuiltTypes.Add(type); - return type.BuiltName; - } - - return name; - } - - internal static bool IsValidDotnetName(string? name) - { - if (name == null) - return false; - - return Regex.IsMatch(name, @"^[a-zA-Z@_](?>\w|@)+?$"); - } - - private static string Lower(bool val) - => val.ToString().ToLower(); - private static bool IsEnum(Type t) - => t.IsScalar && (t.Extending?.StartsWith("enum") ?? false); - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/ClassBuilderContext.cs b/tools/EdgeDB.DotnetTool/Schemas/ClassBuilderContext.cs deleted file mode 100644 index ce03394f..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/ClassBuilderContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal class ClassBuilderContext - { - public List BuiltTypes { get; set; } = new(); - - public List BuildProperties { get; set; } = new(); - - public Func NameCallback { get; set; } = (s) => s; - - public List RequestedAttributes { get; set; } = new(); - - public Type? Type { get; set; } - - public Module? Module { get; set; } - - public Property? Property { get; set; } - - public string? OutputDir { get; set; } - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/Models/Annotation.cs b/tools/EdgeDB.DotnetTool/Schemas/Models/Annotation.cs deleted file mode 100644 index 98071ee7..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/Models/Annotation.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal class Annotation - { - public string? Title { get; set; } - - public string? Description { get; set; } - - public string? Deprecated { get; set; } - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/Models/Constraint.cs b/tools/EdgeDB.DotnetTool/Schemas/Models/Constraint.cs deleted file mode 100644 index d2ac2769..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/Models/Constraint.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal class Constraint - { - public string? Value { get; set; } - - public bool IsExpression { get; set; } - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/Models/Module.cs b/tools/EdgeDB.DotnetTool/Schemas/Models/Module.cs deleted file mode 100644 index c4317935..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/Models/Module.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal class Module - { - public string? Name { get; set; } - - public List Types { get; set; } = new(); - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/Models/Property.cs b/tools/EdgeDB.DotnetTool/Schemas/Models/Property.cs deleted file mode 100644 index 26cca462..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/Models/Property.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace EdgeDB.DotnetTool -{ - internal class Property - { - public Type? Parent { get; set; } - - public string? Name { get; set; } - - public string? Type { get; set; } - - public bool Required { get; set; } - - public PropertyCardinality Cardinality { get; set; } - - public string? DefaultValue { get; set; } - - public bool ReadOnly { get; set; } - - public List Constraints { get; set; } = new(); - - public Annotation? Annotation { get; set; } - - public List LinkProperties { get; set; } = new(); - - public bool IsStrictlyConstraint { get; set; } - - public bool IsAbstract { get; set; } - - public bool IsLink { get; set; } - - public bool IsComputed { get; set; } - - public string? ComputedValue { get; set; } - - public string? Extending { get; set; } - - // used for builder - public string? BuiltName { get; set; } - } - - public enum PropertyCardinality - { - One, - Multi - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/Models/Type.cs b/tools/EdgeDB.DotnetTool/Schemas/Models/Type.cs deleted file mode 100644 index aa0f65c2..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/Models/Type.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal class Type - { - public Module? Parent { get; set; } - - public string? Name { get; set; } - - public string? Extending { get; set; } - - public bool IsAbstract { get; set; } - - public bool IsScalar { get; set; } - - public bool IsLink { get; set; } - - public List Properties { get; set; } = new(); - - // used for builder - public string? BuiltName { get; set; } - } -} diff --git a/tools/EdgeDB.DotnetTool/Schemas/SchemaReader.cs b/tools/EdgeDB.DotnetTool/Schemas/SchemaReader.cs deleted file mode 100644 index d00ddd57..00000000 --- a/tools/EdgeDB.DotnetTool/Schemas/SchemaReader.cs +++ /dev/null @@ -1,333 +0,0 @@ -using EdgeDB.DotnetTool.Lexer; - -namespace EdgeDB.DotnetTool -{ - internal class SchemaReader - { - private readonly SchemaLexer _lexer; - - public SchemaReader(string schema) - { - _lexer = new(schema); - } - - public List Read() - { - List modules = new(); - while (_lexer.PeekToken().Type != TokenType.EndOfFile) - { - modules.Add(ReadModule()); - } - return modules; - } - - private Module ReadModule() - { - var module = _lexer.Expect(TokenType.Module); - _lexer.Expect(TokenType.BeginBrace); - - var ret = new Module - { - Name = module.Value - }; - - while (_lexer.PeekToken().Type != TokenType.EndBrace) - { - var type = ReadModuleType(ret); - if (type != null) - ret.Types.Add(type); - } - - ExpectEndOfBody(); - - return ret; - } - - private Type? ReadModuleType(Module module) - { - bool isScalar = false; - bool isAbstract = false; - - var type = _lexer.ReadToken(); - - if (type.Type == TokenType.Scalar) - { - isScalar = true; - type = _lexer.ReadToken(); - } - - if (type.Type == TokenType.Abstract) - { - isAbstract = true; - type = _lexer.ReadToken(); - } - - switch (type.Type) - { - case TokenType.Type: - return ReadType(module, type, isScalar, isAbstract); - case TokenType.Annotation when isAbstract: - { - // skip, useless - // Colin 03-04-2022 - _ = _lexer.ReadToken(); // read value - _ = _lexer.ReadToken(); // skip semicolon - return null; - } - case TokenType.Constraint when isAbstract: - { - // store somewhere - return null; - } - case TokenType.Function: - { - // store somewhere - return null; - } - case TokenType.Link: - { - return ReadType(module, type, isScalar, isAbstract, true); - } - case TokenType.EndOfFile: - { - // shouldn't happen? - return null; - } - default: - { - // skip? - - return null; - } - } - } - - private Type? ReadType(Module module, Token type, bool isScalar, bool isAbstract, bool isLink = false) - { - var ret = new Type - { - Parent = module, - Name = type.Value, - IsScalar = isScalar, - IsAbstract = isAbstract, - IsLink = isLink, - }; - - if (_lexer.PeekToken().Type == TokenType.Semicolon) - { - // empty shape? - _lexer.Expect(TokenType.Semicolon); - return null; - } - - while (_lexer.PeekToken().Type != TokenType.BeginBrace) - { - var other = _lexer.ReadToken(); - - switch (other.Type) - { - case TokenType.Extending: - { - if (other.Value!.StartsWith("enum<")) - { - var t = new List(); - while (_lexer.PeekToken().Type == TokenType.Identifier) - { - t.Add(_lexer.ReadToken()); - } - - ret.Extending = $"{other.Value} {string.Join(" ", t.Select(x => x.Value))}"; - // read semi colon - if (_lexer.PeekToken().Type == TokenType.Semicolon) - _lexer.ReadToken(); - - - goto endLabel; - } - else - { - ret.IsAbstract = true; - ret.Extending = other.Value; - - // read semi colon - if (_lexer.PeekToken().Type == TokenType.Semicolon) - _lexer.ReadToken(); - - if (_lexer.PeekToken().Type == TokenType.Comma) - { - while (_lexer.PeekToken().Type != TokenType.BeginBrace) - { - _lexer.ReadToken(); // comma - var a = _lexer.ReadToken(); - ret.Extending += $", {a.Value}"; - } - } - - goto endLabel; - } - } - default: - throw new FormatException($"Unexpected token type {other.Type} at {other.StartLine}:{other.StartPos}"); - } - } - - endLabel: - - if (_lexer.PeekToken().Type == TokenType.BeginBrace) - { - _lexer.Expect(TokenType.BeginBrace); - // read inner - while (_lexer.PeekToken().Type != TokenType.EndBrace) - { - ret.Properties.Add(ReadProperty(ret)); - } - - ExpectEndOfBody(); - } - - return ret; - } - - private Property ReadProperty(Type type) - { - var prop = new Property() { Parent = type }; - - while (_lexer.PeekToken().Type is not TokenType.Property and not TokenType.Link) - { - var token = _lexer.ReadToken(); - - switch (token.Type) - { - case TokenType.Required: - prop.Required = true; - break; - case TokenType.Single: - prop.Cardinality = PropertyCardinality.One; - break; - case TokenType.Multi: - prop.Cardinality = PropertyCardinality.Multi; - break; - case TokenType.Abstract: - prop.IsAbstract = true; - break; - case TokenType.Constraint: - prop.IsStrictlyConstraint = true; - prop.Constraints.Add(new Constraint - { - Value = token.Value, - IsExpression = false, - }); - - if (_lexer.PeekToken().Type == TokenType.Semicolon) - _lexer.ReadToken(); - - return prop; - } - } - - var propDeclaration = _lexer.ReadToken(); - prop.Name = propDeclaration.Value; - prop.IsLink = propDeclaration.Type == TokenType.Link; - - // read type - var propertyDeclarerToken = _lexer.ReadToken(); - if (propertyDeclarerToken.Type == TokenType.TypeArrow) - { - prop.Type = propertyDeclarerToken.Value; - } - else if (propertyDeclarerToken.Type == TokenType.Assignment) - { - prop.IsComputed = true; - prop.ComputedValue = propertyDeclarerToken.Value; - } - else if (propertyDeclarerToken.Type == TokenType.Extending) - { - // read the extending value until type assignment - var typeAssignment = _lexer.Expect(TokenType.TypeArrow); - prop.Extending = propertyDeclarerToken.Value; - prop.Type = typeAssignment.Value; - } - else - throw new FormatException($"Expected type arrow or assignment but got {propertyDeclarerToken.Type} at {propertyDeclarerToken.StartLine}:{propertyDeclarerToken.StartPos}"); - - - // check for body - if (_lexer.PeekToken().Type == TokenType.BeginBrace) - { - _lexer.Expect(TokenType.BeginBrace); - - while (_lexer.PeekToken().Type != TokenType.EndBrace) - { - var peeked = _lexer.PeekToken(); - - switch (peeked.Type) - { - case TokenType.Constraint: - prop.Constraints.Add(ReadConstraint()); - break; - case TokenType.Annotation: - prop.Annotation = ReadAnnotation(); - break; - case TokenType.Property when prop.IsLink: - prop.LinkProperties.Add(ReadProperty(type)); - break; - case TokenType.Identifier when peeked.Value == "default": - { - _lexer.Expect(TokenType.Identifier); - var assignment = _lexer.Expect(TokenType.Assignment); - - if (_lexer.PeekToken().Type == TokenType.Semicolon) - _lexer.ReadToken(); - - prop.DefaultValue = assignment.Value; - } - break; - - default: - throw new FormatException($"Unexpected token, expected constraint or annotation but got {peeked.Type} at {peeked.StartLine}:{peeked.StartPos}"); - } - } - - ExpectEndOfBody(); - } - else - _lexer.Expect(TokenType.Semicolon); - - return prop; - } - - private Constraint ReadConstraint() - { - var constraint = _lexer.Expect(TokenType.Constraint); - - if (_lexer.PeekToken().Type == TokenType.On && constraint.Value == "expression") - { - //var val = ""; - _lexer.Expect(TokenType.BeginParenthesis); - - return new Constraint - { - IsExpression = true, - }; - } - - if (_lexer.PeekToken().Type == TokenType.Semicolon) - _lexer.ReadToken(); - - return new Constraint - { - IsExpression = false, - Value = constraint.Value - }; - } - - private static Annotation ReadAnnotation() => new() { }; - - private void ExpectEndOfBody() - { - _lexer.Expect(TokenType.EndBrace); - if (_lexer.PeekToken().Type == TokenType.Semicolon) - _lexer.ReadToken(); - } - } -} diff --git a/tools/EdgeDB.DotnetTool/Util/CodeWriter.cs b/tools/EdgeDB.DotnetTool/Util/CodeWriter.cs deleted file mode 100644 index 4c341d8d..00000000 --- a/tools/EdgeDB.DotnetTool/Util/CodeWriter.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal class CodeWriter - { - public readonly StringBuilder Content = new(); - - public int IndentLevel { get; private set; } - - private readonly ScopeTracker _scopeTracker; //We only need one. It can be reused. - - public CodeWriter() - { - _scopeTracker = new(this); //We only need one. It can be reused. - } - - public void Append(string line) - => Content.Append(line); - - public void AppendLine(string line) - => Content.Append(new string(' ', IndentLevel)).AppendLine(line); - - public void AppendLine() - => Content.AppendLine(); - - public IDisposable BeginScope(string line) - { - AppendLine(line); - return BeginScope(); - } - - public IDisposable BeginScope() - { - Content.Append(new string(' ', IndentLevel)).AppendLine("{"); - IndentLevel += 4; - return _scopeTracker; - } - - public void EndLine() => Content.AppendLine(); - - public void EndScope() - { - IndentLevel -= 4; - Content.Append(new string(' ', IndentLevel)).AppendLine("}"); - } - - public void StartLine() - => Content.Append(new string(' ', IndentLevel)); - - public override string ToString() - => Content.ToString(); - - class ScopeTracker : IDisposable - { - public ScopeTracker(CodeWriter parent) - { - Parent = parent; - } - - public CodeWriter Parent { get; } - - public void Dispose() - { - Parent.EndScope(); - } - } - } -} diff --git a/tools/EdgeDB.DotnetTool/Util/PascalUtils.cs b/tools/EdgeDB.DotnetTool/Util/PascalUtils.cs deleted file mode 100644 index 36c7dc5f..00000000 --- a/tools/EdgeDB.DotnetTool/Util/PascalUtils.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading.Tasks; - -namespace EdgeDB.DotnetTool -{ - internal static class PascalUtils - { - readonly static Regex _invalidCharsRgx = new("[^_a-zA-Z0-9]", RegexOptions.Compiled); - - readonly static Regex _whiteSpace = new(@"(?<=\s)", RegexOptions.Compiled); - - readonly static Regex _startsWithLowerCaseChar = new("^[a-z]", RegexOptions.Compiled); - - readonly static Regex _firstCharFollowedByUpperCasesOnly = new("(?<=[A-Z])[A-Z0-9]+$", RegexOptions.Compiled); - - readonly static Regex _lowerCaseNextToNumber = new("(?<=[0-9])[a-z]", RegexOptions.Compiled); - - readonly static Regex _upperCaseInside = new("(?<=[A-Z])[A-Z]+?((?=[A-Z][a-z])|(?=[0-9]))", RegexOptions.Compiled); - - public static string ToPascalCase(string? original) - { - if (original == null) - return ""; - - // replace white spaces with undescore, then replace all invalid chars with empty string - var pascalCase = _invalidCharsRgx.Replace(_whiteSpace.Replace(original, "_"), string.Empty) - // split by underscores - .Split(new char[] { '_' }, StringSplitOptions.RemoveEmptyEntries) - // set first letter to uppercase - .Select(w => _startsWithLowerCaseChar.Replace(w, m => m.Value.ToUpper())) - // replace second and all following upper case letters to lower if there is no next lower (ABC -> Abc) - .Select(w => _firstCharFollowedByUpperCasesOnly.Replace(w, m => m.Value.ToLower())) - // set upper case the first lower case following a number (Ab9cd -> Ab9Cd) - .Select(w => _lowerCaseNextToNumber.Replace(w, m => m.Value.ToUpper())) - // lower second and next upper case letters except the last if it follows by any lower (ABcDEf -> AbcDef) - .Select(w => _upperCaseInside.Replace(w, m => m.Value.ToLower())); - - return string.Concat(pascalCase); - } - } -} diff --git a/tools/EdgeDB.DotnetTool/dbschema/default.esdl b/tools/EdgeDB.DotnetTool/dbschema/default.esdl deleted file mode 100644 index 2d960308..00000000 --- a/tools/EdgeDB.DotnetTool/dbschema/default.esdl +++ /dev/null @@ -1,141 +0,0 @@ -module default { - - scalar type Genre extending enum; - - abstract link movie_character { - property character_name -> str; - } - abstract type Person { - required property name -> str { - constraint exclusive; - }; - } - - type Villain extending Person { - link nemesis -> Hero; - } - - type Hero extending Person { - property secret_identity -> str; - property number_of_movies -> int64; - multi link villains := . Genre; - property rating -> float64; - required property title -> str { - constraint exclusive; - }; - required property release_year -> int16 { - default := datetime_get(datetime_current(), 'year'); - } - multi link characters extending movie_character -> Person; - link profile -> Profile { - constraint exclusive; - } - } - - type Profile { - property plot_summary -> str; - } - - type User { - required property username -> str; - required link favourite_movie -> Movie; - } - - type MovieShape { - } - - abstract type HasName { - property name -> str; - } - abstract type HasAge { - property age -> int64; - } - - scalar type bag_seq extending sequence; - - type Bag extending HasName, HasAge { - property secret_identity -> str; - property genre -> Genre; - property boolField -> bool; - property datetimeField -> datetime; - property localDateField -> cal::local_date; - property localTimeField -> cal::local_time; - property localDateTimeField -> cal::local_datetime; - property durationField -> duration; - property decimalField -> decimal; - property int64Field -> int64; - property int32Field -> int32; - property int16Field -> int16; - property float32Field -> float32; - property float64Field -> float64; - property bigintField -> bigint; - required multi property stringsMulti -> str; - property stringsArr -> array; - multi property stringMultiArr -> array; - property namedTuple -> tuple; - property unnamedTuple -> tuple; - property enumArr -> array; - property seqField -> bag_seq; - } - - type Simple extending HasName, HasAge {} - - # Unicode handling - # https://github.com/edgedb/edgedb/blob/master/tests/schemas/dump02_default.esdl - - abstract annotation `🍿`; - - abstract constraint `🚀🍿`(max: int64) extending max_len_value; - - function `💯`(NAMED ONLY `🙀`: int64) -> int64 { - using ( - SELECT 100 - `🙀` - ); - - annotation `🍿` := 'fun!🚀'; - volatility := 'Immutable'; - } - - type `S p a M` { - required property `🚀` -> int32; - property c100 := (SELECT `💯`(`🙀` := .`🚀`)); - } - - type A { - required link `s p A m 🤞` -> `S p a M`; - } - - scalar type 你好 extending str; - - scalar type مرحبا extending 你好 { - constraint `🚀🍿`(100); - }; - - scalar type `🚀🚀🚀` extending مرحبا; - - type Łukasz { - required property `Ł🤞` -> `🚀🚀🚀` { - default := <`🚀🚀🚀`>'你好🤞' - } - index on (.`Ł🤞`); - - link `Ł💯` -> A { - property `🙀🚀🚀🚀🙀` -> `🚀🚀🚀`; - property `🙀مرحبا🙀` -> مرحبا { - constraint `🚀🍿`(200); - } - }; - } - -}; - -module `💯💯💯` { - function `🚀🙀🚀`(`🤞`: default::`🚀🚀🚀`) -> default::`🚀🚀🚀` - using ( - SELECT (`🤞` ++ 'Ł🙀') - ); -}; \ No newline at end of file diff --git a/tools/EdgeDB.DotnetTool/dbschema/migrations/00001.edgeql b/tools/EdgeDB.DotnetTool/dbschema/migrations/00001.edgeql deleted file mode 100644 index 6232b5a9..00000000 --- a/tools/EdgeDB.DotnetTool/dbschema/migrations/00001.edgeql +++ /dev/null @@ -1,117 +0,0 @@ -CREATE MIGRATION m1g3fpbyrx4lwrodm7wrekqq4pkbz6otz7gcwmwz43l5mca3qtc47q - ONTO initial -{ - CREATE MODULE `💯💯💯` IF NOT EXISTS; - CREATE ABSTRACT ANNOTATION default::`🍿`; - CREATE FUNCTION default::`💯`(NAMED ONLY `🙀`: std::int64) -> std::int64 { - SET volatility := 'Immutable'; - CREATE ANNOTATION default::`🍿` := 'fun!🚀'; - USING (SELECT - (100 - `🙀`) - ) - ;}; - CREATE SCALAR TYPE default::Genre EXTENDING enum; - CREATE ABSTRACT TYPE default::HasAge { - CREATE PROPERTY age -> std::int64; - }; - CREATE ABSTRACT TYPE default::HasName { - CREATE PROPERTY name -> std::str; - }; - CREATE SCALAR TYPE default::bag_seq EXTENDING std::sequence; - CREATE TYPE default::Bag EXTENDING default::HasName, default::HasAge { - CREATE PROPERTY enumArr -> array; - CREATE PROPERTY bigintField -> std::bigint; - CREATE PROPERTY boolField -> std::bool; - CREATE PROPERTY datetimeField -> std::datetime; - CREATE PROPERTY decimalField -> std::decimal; - CREATE PROPERTY durationField -> std::duration; - CREATE PROPERTY float32Field -> std::float32; - CREATE PROPERTY float64Field -> std::float64; - CREATE PROPERTY genre -> default::Genre; - CREATE PROPERTY int16Field -> std::int16; - CREATE PROPERTY int32Field -> std::int32; - CREATE PROPERTY int64Field -> std::int64; - CREATE PROPERTY localDateField -> cal::local_date; - CREATE PROPERTY localDateTimeField -> cal::local_datetime; - CREATE PROPERTY localTimeField -> cal::local_time; - CREATE PROPERTY namedTuple -> tuple; - CREATE PROPERTY secret_identity -> std::str; - CREATE PROPERTY seqField -> default::bag_seq; - CREATE MULTI PROPERTY stringMultiArr -> array; - CREATE PROPERTY stringsArr -> array; - CREATE REQUIRED MULTI PROPERTY stringsMulti -> std::str; - CREATE PROPERTY unnamedTuple -> tuple; - }; - CREATE ABSTRACT CONSTRAINT default::`🚀🍿`(max: std::int64) EXTENDING std::max_len_value; - CREATE TYPE default::A; - CREATE SCALAR TYPE default::你好 EXTENDING std::str; - CREATE SCALAR TYPE default::مرحبا EXTENDING default::你好 { - CREATE CONSTRAINT default::`🚀🍿`(100); - }; - CREATE SCALAR TYPE default::`🚀🚀🚀` EXTENDING default::مرحبا; - CREATE TYPE default::Łukasz { - CREATE LINK `Ł💯` -> default::A { - CREATE PROPERTY `🙀مرحبا🙀` -> default::مرحبا { - CREATE CONSTRAINT default::`🚀🍿`(200); - }; - CREATE PROPERTY `🙀🚀🚀🚀🙀` -> default::`🚀🚀🚀`; - }; - CREATE REQUIRED PROPERTY `Ł🤞` -> default::`🚀🚀🚀` { - SET default := ('你好🤞'); - }; - CREATE INDEX ON (.`Ł🤞`); - }; - CREATE TYPE default::`S p a M` { - CREATE REQUIRED PROPERTY `🚀` -> std::int32; - CREATE PROPERTY c100 := (SELECT - default::`💯`(`🙀` := .`🚀`) - ); - }; - CREATE FUNCTION `💯💯💯`::`🚀🙀🚀`(`🤞`: default::`🚀🚀🚀`) -> default::`🚀🚀🚀` USING (SELECT - (`🤞` ++ 'Ł🙀') - ); - CREATE ABSTRACT LINK default::movie_character { - CREATE PROPERTY character_name -> std::str; - }; - CREATE ABSTRACT TYPE default::Person { - CREATE REQUIRED PROPERTY name -> std::str { - CREATE CONSTRAINT std::exclusive; - }; - }; - CREATE TYPE default::Profile { - CREATE PROPERTY plot_summary -> std::str; - }; - CREATE TYPE default::Movie { - CREATE MULTI LINK characters EXTENDING default::movie_character -> default::Person; - CREATE LINK profile -> default::Profile { - CREATE CONSTRAINT std::exclusive; - }; - CREATE PROPERTY genre -> default::Genre; - CREATE PROPERTY rating -> std::float64; - CREATE REQUIRED PROPERTY release_year -> std::int16 { - SET default := (std::datetime_get(std::datetime_current(), 'year')); - }; - CREATE REQUIRED PROPERTY title -> std::str { - CREATE CONSTRAINT std::exclusive; - }; - }; - ALTER TYPE default::A { - CREATE REQUIRED LINK `s p A m 🤞` -> default::`S p a M`; - }; - CREATE TYPE default::Simple EXTENDING default::HasName, default::HasAge; - CREATE TYPE default::Hero EXTENDING default::Person { - CREATE PROPERTY number_of_movies -> std::int64; - CREATE PROPERTY secret_identity -> std::str; - }; - CREATE TYPE default::Villain EXTENDING default::Person { - CREATE LINK nemesis -> default::Hero; - }; - ALTER TYPE default::Hero { - CREATE MULTI LINK villains := (. default::Movie; - CREATE REQUIRED PROPERTY username -> std::str; - }; - CREATE TYPE default::MovieShape; -}; diff --git a/tools/EdgeDB.DotnetTool/edgedb.toml b/tools/EdgeDB.DotnetTool/edgedb.toml deleted file mode 100644 index 597ff333..00000000 --- a/tools/EdgeDB.DotnetTool/edgedb.toml +++ /dev/null @@ -1,2 +0,0 @@ -[edgedb] -server-version = "1.1" diff --git a/tools/EdgeDB.DotnetTool/test.esdl b/tools/EdgeDB.DotnetTool/test.esdl deleted file mode 100644 index b67692cc..00000000 --- a/tools/EdgeDB.DotnetTool/test.esdl +++ /dev/null @@ -1,113 +0,0 @@ -module default { - abstract annotation `🍿`; - abstract constraint `🚀🍿`(max: std::int64) extending std::max_len_value; - function `💯`(named only `🙀`: std::int64) -> std::int64 { - volatility := 'Immutable'; - annotation default::`🍿` := 'fun!🚀'; - using (select - (100 - `🙀`) - ) - ;}; - abstract link movie_character { - property character_name -> std::str; - }; - scalar type Genre extending enum; - scalar type bag_seq extending std::sequence; - scalar type مرحبا extending default::你好 { - constraint default::`🚀🍿`(100); - }; - scalar type 你好 extending std::str; - scalar type `🚀🚀🚀` extending default::مرحبا; - type A { - required link `s p A m 🤞` -> default::`S p a M`; - }; - type Bag extending default::HasName, default::HasAge { - property bigintField -> std::bigint; - property boolField -> std::bool; - property datetimeField -> std::datetime; - property decimalField -> std::decimal; - property durationField -> std::duration; - property enumArr -> array; - property float32Field -> std::float32; - property float64Field -> std::float64; - property genre -> default::Genre; - property int16Field -> std::int16; - property int32Field -> std::int32; - property int64Field -> std::int64; - property localDateField -> cal::local_date; - property localDateTimeField -> cal::local_datetime; - property localTimeField -> cal::local_time; - property namedTuple -> tuple; - property secret_identity -> std::str; - property seqField -> default::bag_seq; - multi property stringMultiArr -> array; - property stringsArr -> array; - required multi property stringsMulti -> std::str; - property unnamedTuple -> tuple; - }; - abstract type HasAge { - property age -> std::int64; - }; - abstract type HasName { - property name -> std::str; - }; - type Hero extending default::Person { - multi link villains := (. std::int64; - property secret_identity -> std::str; - }; - type Movie { - multi link characters extending default::movie_character -> default::Person; - link profile -> default::Profile { - constraint std::exclusive; - }; - property genre -> default::Genre; - property rating -> std::float64; - required property release_year -> std::int16 { - default := (std::datetime_get(std::datetime_current(), 'year')); - }; - required property title -> std::str { - constraint std::exclusive; - }; - }; - type MovieShape; - abstract type Person { - required property name -> std::str { - constraint std::exclusive; - }; - }; - type Profile { - property plot_summary -> std::str; - }; - type `S p a M` { - property c100 := (select - default::`💯`(`🙀` := .`🚀`) - ); - required property `🚀` -> std::int32; - }; - type Simple extending default::HasName, default::HasAge; - type User { - required link favourite_movie -> default::Movie; - required property username -> std::str; - }; - type Villain extending default::Person { - link nemesis -> default::Hero; - }; - type Łukasz { - index on (.`Ł🤞`); - link `Ł💯` -> default::A { - property `🙀مرحبا🙀` -> default::مرحبا { - constraint default::`🚀🍿`(200); - }; - property `🙀🚀🚀🚀🙀` -> default::`🚀🚀🚀`; - }; - required property `Ł🤞` -> default::`🚀🚀🚀` { - default := ('你好🤞'); - }; - }; -}; -module `💯💯💯` { - function `🚀🙀🚀`(`🤞`: default::`🚀🚀🚀`) -> default::`🚀🚀🚀` using (select - (`🤞` ++ 'Ł🙀') - ); -}; \ No newline at end of file