diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..30556aa
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,104 @@
+*.txt text eol=crlf diff
+*.xml text eol=crlf diff
+*.xaml text eol=crlf diff
+*.dll -text -diff
+*.pdb -text -diff
+*.zip -text -diff
+*.config text eol=crlf diff
+*.docx -text -diff
+*.sample text eol=crlf diff
+*.chm -text -diff
+*.jar -text -diff
+*.sln text eol=crlf diff
+*.vssscc text eol=crlf diff
+*.testsettings text eol=crlf diff
+*.vsmdi text eol=crlf diff
+*.targets text eol=crlf diff
+*.bat text eol=crlf diff
+*.idx -text -diff
+*.pack -text -diff
+*.698_20141017 text eol=crlf diff
+*.181_20141104 text eol=crlf diff
+*.698_20141105 text eol=crlf diff
+*.701_20141106 text eol=crlf diff
+*.488_20141110 text eol=crlf diff
+*.197_20141117 text eol=crlf diff
+*.190_20141016 text eol=crlf diff
+*.csproj text eol=crlf diff
+*.vspscc text eol=crlf diff
+*.cs text eol=crlf diff
+*.settings text eol=crlf diff
+*.Config text eol=crlf diff
+*.exe -text -diff
+*.aspx text eol=crlf diff
+*.vb text eol=crlf diff
+*.asax text eol=crlf diff
+*.ashx text eol=crlf diff
+*.ico -text -diff
+*.html text eol=crlf diff
+*.vbproj text eol=crlf diff
+*.csv text eol=crlf diff
+*.loadtest text eol=crlf diff
+*.webtest text eol=crlf diff
+*.cd text eol=crlf diff
+*.jmx text eol=crlf diff
+*.css text eol=crlf diff
+*.json text eol=crlf diff
+*.js text eol=crlf diff
+*.XML text eol=crlf diff
+*.prefs text eol=crlf diff
+*.tsv text eol=crlf diff
+*.xlsx -text -diff
+*.au3 text eol=crlf diff
+*.donotuse -text -diff
+*.png -text -diff
+*.nupkg -text -diff
+*.htm text eol=crlf diff
+*.browser text eol=crlf diff
+*.exclude text eol=crlf diff
+*.gif -text -diff
+*.jpg -text -diff
+*.mmdb -text -diff
+*.myapp text eol=crlf diff
+*.resx text eol=crlf diff
+*.rb text eol=crlf diff
+*.yaml text eol=crlf diff
+*.manifest text eol=crlf diff
+*.sql text eol=crlf diff
+*.markdown text eol=crlf diff
+*.launch text eol=crlf diff
+*.crx -text -diff
+*.xpi -text -diff
+*.ps1 text eol=crlf diff
+*.ascx text eol=crlf diff
+*.master text eol=crlf diff
+*.disco text eol=crlf diff
+*.discomap text eol=crlf diff
+*.wsdl text eol=crlf diff
+*.htc text eol=crlf diff
+*.pubxml text eol=crlf diff
+*.datasource text eol=crlf diff
+*.map text eol=crlf diff
+*.ts text eol=crlf diff
+*.cshtml text eol=crlf diff
+*.nuspec text eol=crlf diff
+*.pp text eol=crlf diff
+*.opts text eol=crlf diff
+*.erb text eol=crlf diff
+*.pri -text -diff
+*.inc text eol=crlf diff
+*.db -text -diff
+*.html-20090130 text eol=crlf diff
+*.Master text eol=crlf diff
+*.rej text eol=crlf diff
+*.psd -text -diff
+*.bmp -text -diff
+*.properties text eol=crlf diff
+*.task text eol=crlf diff
+*.project text eol=crlf diff
+*.classpath text eol=crlf diff
+*.buildpath text eol=crlf diff
+*.java text eol=crlf diff
+*.sql encoding=UTF-8
+*.cshtml encoding=UTF-8
+*.cs encoding=UTF-8
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..971a07a
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,34 @@
+name: .NET Core Build
+
+on:
+ pull_request:
+
+env:
+ NETCORE_VERSION: '8.0'
+ GIT_REPO_ROOT: src
+ SOLUTION_FILE: Agoda.GrapqhlGen.sln
+ DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
+
+jobs:
+
+ build:
+ name: Build Package
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET Core SDK ${{ env.NETCORE_VERSION }}
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: ${{ env.NETCORE_VERSION }}
+
+ - name: Restore
+ working-directory: ${{ env.GIT_REPO_ROOT }}
+ run: dotnet restore ${{ env.SOLUTION_FILE }}
+
+ - name: Add MSBuild to PATH
+ uses: microsoft/setup-msbuild@v1
+
+ - name: Build
+ working-directory: ${{ env.GIT_REPO_ROOT }}
+ run: dotnet build ${{ env.SOLUTION_FILE }} --configuration Debug --no-restore
\ No newline at end of file
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..fb9dd42
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,98 @@
+name: .NET Core Build and Publish
+
+on:
+ push:
+
+env:
+ NETCORE_VERSION: '8.0'
+ GIT_REPO_ROOT: src
+ MAJOR_MINOR_VERSION: 1.0.
+ SOLUTION_FILE: Agoda.GrapqhlGen.sln
+ DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX: 2
+
+jobs:
+
+ build:
+ name: Build Package
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Setup .NET Core SDK ${{ env.NETCORE_VERSION }}
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: ${{ env.NETCORE_VERSION }}
+
+ - name: Restore
+ working-directory: ${{ env.GIT_REPO_ROOT }}
+ run: dotnet restore ${{ env.SOLUTION_FILE }}
+
+ - name: Add MSBuild to PATH
+ uses: microsoft/setup-msbuild@v1
+
+ - name: Build
+ working-directory: ${{ env.GIT_REPO_ROOT }}
+ run: dotnet build ${{ env.SOLUTION_FILE }} --configuration Release --no-restore
+
+ - name: Pack Release
+ if: github.ref == 'refs/heads/master'
+ working-directory: ${{ env.GIT_REPO_ROOT }}
+ run: dotnet pack ${{ env.SOLUTION_FILE }} --configuration Release -o finalpackage --no-build -p:PackageVersion=${{ env.MAJOR_MINOR_VERSION }}${{ github.run_number }}
+
+ - name: Pack Preview
+ if: github.ref != 'refs/heads/master'
+ working-directory: ${{ env.GIT_REPO_ROOT }}
+ run: dotnet pack ${{ env.SOLUTION_FILE }} --configuration Release -o finalpackage --no-build -p:PackageVersion=${{ env.MAJOR_MINOR_VERSION }}${{ github.run_number }}-preview
+
+ - name: Publish artifact
+ uses: actions/upload-artifact@master
+ with:
+ name: nupkg
+ path: ${{ env.GIT_REPO_ROOT }}/finalpackage
+
+ deploy:
+ needs: build
+ name: Deploy Packages
+ runs-on: ubuntu-latest
+ steps:
+ - name: Download Package artifact
+ uses: actions/download-artifact@master
+ with:
+ name: nupkg
+ path: ./nupkg
+
+ - name: Setup NuGet
+ uses: NuGet/setup-nuget@v1.0.5
+ with:
+ nuget-api-key: ${{ secrets.KLONDIKE_API_KEY }}
+ nuget-version: latest
+
+ - name: Setup .NET Core SDK
+ uses: actions/setup-dotnet@v1
+ with:
+ dotnet-version: ${{ env.NETCORE_VERSION }}
+
+ - name: Push to NuGet
+ run: dotnet nuget push nupkg/**/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org --skip-duplicate
+
+ release:
+ needs: deploy
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/master'
+ steps:
+ - name: Create Draft Release
+ id: create_release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ env.MAJOR_MINOR_VERSION }}${{ github.run_number }}
+ release_name: ${{ env.MAJOR_MINOR_VERSION }}${{ github.run_number }}
+ draft: true
+ prerelease: false
+
+ - uses: eregon/publish-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ release_id: ${{ steps.create_release.outputs.id }}
\ No newline at end of file
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..e69de29
diff --git a/README.md b/README.md
index 83e1765..6a0147f 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,72 @@
-# dotnet-cli-grapqhl-gen
-Grapqhl Generation dotnet cli Tool
+# dotnet-cli-graphql-gen: GraphQL Code Generation Made Easy!
+
+## Overview
+
+Welcome to dotnet-cli-graphql-gen, an open-source CLI tool that wraps npm package graphql-codegen to add some missing parts.
+
+## Requirements
+
+- .NET 8.0 or higher (Because we believe in staying current!)
+- pnpm (Don't worry, we'll install it if you don't have it)
+
+## Installation
+
+### Global Tool Installation
+
+Install it globally (because great tools should be available everywhere):
+```bash
+dotnet tool install --global Agoda.GrapqhlGen
+```
+
+### Local Project Installation
+
+Or keep it project-specific:
+```bash
+dotnet new tool-manifest # if you haven't already
+dotnet tool install Agoda.GrapqhlGen
+```
+
+## Quick Start
+
+Generate your GraphQL client code with a single command:
+
+```bash
+grapqhlgen \
+ --schema-url "https://your.graphql.api" \
+ --input-path "./graphql" \
+ --output-path "./Generated" \
+ --namespace "YourCompany.YourProject" \
+ --headers "API-Key: your-key"
+```
+
+## Command Options
+
+- `--schema-url` (Required): URL of your GraphQL schema
+- `--input-path` (Required): Directory containing your .graphql files
+- `--output-path` (Required): Where to save generated files
+- `--namespace` (Optional): Base namespace for generated code (Default: "Generated")
+- `--headers` (Optional): Headers for schema request (Format: "Key: Value")
+- `--template` (Optional): Code generation template (Default: "typescript")
+- `--model-file` (Optional): Name of the generated models file (Default: "Models.cs")
+- `--log-level` (Optional): Set logging verbosity (Default: "Information")
+
+## Contributing
+
+We love contributions! Whether you're fixing bugs, improving documentation, or adding new features, check out our [Contributing Guide](CONTRIBUTING.md) for details on getting started.
+
+## Best Practices
+
+- Keep your GraphQL queries in separate .graphql files
+- Use meaningful names for your queries and mutations
+- Organize your GraphQL files by feature or domain
+- Version control your GraphQL files alongside your code
+
+## And Finally...
+
+Remember, in the world of GraphQL, there are two types of developers: those who use code generation tools, and those who wish they had started using them sooner! With dotnet-cli-graphql-gen, you'll never want to write GraphQL client code by hand again.
+
+Happy coding, and may your queries always resolve! 🚀
+
+## License
+
+Apache 2.0 - feel free to use this tool in your projects, whether personal or commercial. Just don't blame us if your GraphQL queries start writing themselves! 😉
\ No newline at end of file
diff --git a/src/Agoda.GrapqhlGen.Tests/Agoda.GrapqhlGen.Tests.csproj b/src/Agoda.GrapqhlGen.Tests/Agoda.GrapqhlGen.Tests.csproj
new file mode 100644
index 0000000..71306fb
--- /dev/null
+++ b/src/Agoda.GrapqhlGen.Tests/Agoda.GrapqhlGen.Tests.csproj
@@ -0,0 +1,26 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
diff --git a/src/Agoda.GrapqhlGen.Tests/CodeGeneratorTests.cs b/src/Agoda.GrapqhlGen.Tests/CodeGeneratorTests.cs
new file mode 100644
index 0000000..ac0b414
--- /dev/null
+++ b/src/Agoda.GrapqhlGen.Tests/CodeGeneratorTests.cs
@@ -0,0 +1,351 @@
+using NUnit.Framework;
+using NSubstitute;
+using Shouldly;
+using System.Text;
+
+namespace Agoda.GrapqhlGen.Tests;
+
+[TestFixture]
+public class CodeGeneratorTests
+{
+ private ICommandExecutor _commandExecutor = null!;
+ private string _tempPath = null!;
+ private CodeGenerator _generator = null!;
+ private const string SchemaUrl = "https://test.graphql";
+
+ [SetUp]
+ public void Setup()
+ {
+ _commandExecutor = Substitute.For();
+ _tempPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
+ Directory.CreateDirectory(_tempPath);
+
+ _generator = new CodeGenerator(
+ SchemaUrl,
+ _tempPath,
+ _tempPath,
+ "Test.Namespace",
+ new Dictionary { { "API-Key", "test-key" } },
+ "typescript",
+ "Models.cs",
+ _commandExecutor);
+ }
+
+ [TearDown]
+ public void Cleanup()
+ {
+ if (Directory.Exists(_tempPath))
+ {
+ Directory.Delete(_tempPath, true);
+ }
+ }
+
+ private void SetupDefaultCommandExecutor()
+ {
+ _commandExecutor
+ .ExecuteAsync(Arg.Any(), Arg.Any())
+ .Returns(callInfo =>
+ {
+ var command = callInfo.ArgAt(0);
+ var args = callInfo.ArgAt(1);
+ Console.WriteLine($"Mock received command: {command} {args}");
+
+ if (args.Contains("graphql-codegen"))
+ {
+ var classesPath = Path.Combine(_tempPath, "Classes.cs");
+ Console.WriteLine($"Creating Classes.cs at {classesPath}");
+ File.WriteAllText(classesPath, @"
+namespace Generated
+{
+ #region input types
+ public class TestModel {}
+ #endregion
+
+ #region TestQuery
+ public class TestQuery {}
+ #endregion
+}");
+ Console.WriteLine($"Classes.cs created: {File.Exists(classesPath)}");
+ }
+ return Task.CompletedTask;
+ });
+ }
+
+ [Test]
+ public async Task GenerateAsync_ShouldCreateExpectedFiles()
+ {
+ // Arrange
+ SetupMockFiles();
+ SetupDefaultCommandExecutor();
+
+ // Act
+ await _generator.GenerateAsync();
+
+ // Debug Output
+ Console.WriteLine($"Temp Path: {_tempPath}");
+ Console.WriteLine($"Files in directory:");
+ foreach (var file in Directory.GetFiles(_tempPath))
+ {
+ Console.WriteLine($"- {file}");
+ }
+
+ // Assert
+ var modelsPath = Path.Combine(_tempPath, "Models.cs");
+ File.Exists(modelsPath).ShouldBeTrue($"Models.cs should exist at {modelsPath}");
+
+ var queryPath = Path.Combine(_tempPath, "TestQuery.generated.cs");
+ File.Exists(queryPath).ShouldBeTrue($"TestQuery.generated.cs should exist at {queryPath}");
+ }
+
+ [Test]
+ public async Task GenerateAsync_ShouldReplaceNamespaceCorrectly()
+ {
+ // Arrange
+ SetupMockFiles();
+ SetupDefaultCommandExecutor();
+
+ // Act
+ await _generator.GenerateAsync();
+
+ // Assert
+ var generatedContent = File.ReadAllText(Path.Combine(_tempPath, "Models.cs"));
+ generatedContent.ShouldContain("namespace Test.Namespace");
+ generatedContent.ShouldNotContain("namespace Generated");
+ }
+
+ [Test]
+ public async Task GenerateAsync_WithNoInputTypesRegion_ShouldNotCreateModelsFile()
+ {
+ // Arrange
+ SetupMockFiles();
+
+ _commandExecutor
+ .ExecuteAsync(Arg.Any(), Arg.Any())
+ .Returns(callInfo =>
+ {
+ if (callInfo.ArgAt(1).Contains("graphql-codegen"))
+ {
+ File.WriteAllText(Path.Combine(_tempPath, "Classes.cs"), @"
+namespace Generated
+{
+ #region TestQuery
+ public class TestQuery {}
+ #endregion
+}");
+ }
+ return Task.CompletedTask;
+ });
+
+ // Act
+ await _generator.GenerateAsync();
+
+ // Assert
+ File.Exists(Path.Combine(_tempPath, "Models.cs")).ShouldBeFalse();
+ File.Exists(Path.Combine(_tempPath, "TestQuery.generated.cs")).ShouldBeTrue();
+ }
+
+ [Test]
+ public async Task GenerateAsync_WithEmptyHeaders_ShouldNotIncludeHeadersInCommand()
+ {
+ // Arrange
+ var generator = new CodeGenerator(
+ SchemaUrl,
+ _tempPath,
+ _tempPath,
+ "Test.Namespace",
+ null,
+ "typescript",
+ "Models.cs",
+ _commandExecutor);
+
+ SetupMockFiles();
+
+ // Setup command executor to create Classes.cs
+ _commandExecutor
+ .ExecuteAsync(Arg.Any(), Arg.Any())
+ .Returns(callInfo =>
+ {
+ var command = callInfo.ArgAt(0);
+ var args = callInfo.ArgAt(1);
+
+ if (args.Contains("graphql-codegen"))
+ {
+ var content = @"
+namespace Generated
+{
+ #region input types
+ public class TestModel {}
+ #endregion
+
+ #region TestQuery
+ public class TestQuery {}
+ #endregion
+}";
+ File.WriteAllText(Path.Combine(_tempPath, "Classes.cs"), content);
+ }
+ return Task.CompletedTask;
+ });
+
+ // Act
+ await generator.GenerateAsync();
+
+ // Assert
+ // Check that none of the pnpm commands contained "--header"
+ await _commandExecutor.Received().ExecuteAsync("pnpm",
+ Arg.Is(s => s == "install @graphql-codegen/cli @graphql-codegen/typescript"));
+
+ await _commandExecutor.Received().ExecuteAsync("pnpm",
+ Arg.Is(s =>
+ s.StartsWith("graphql-codegen") &&
+ !s.Contains("--header")));
+
+ // Additional verification that no other pnpm commands were called
+ await _commandExecutor.Received(2).ExecuteAsync(
+ Arg.Is("pnpm"),
+ Arg.Any());
+
+ // Verify the files are generated correctly
+ File.Exists(Path.Combine(_tempPath, "Models.cs")).ShouldBeTrue();
+ File.Exists(Path.Combine(_tempPath, "TestQuery.generated.cs")).ShouldBeTrue();
+ }
+
+ [Test]
+ public async Task GenerateAsync_ShouldCleanWorkingDirectory()
+ {
+ // Arrange
+ var existingGraphqlFile = Path.Combine(_tempPath, "existing.graphql");
+ var existingClassesFile = Path.Combine(_tempPath, "Classes.cs");
+ File.WriteAllText(existingGraphqlFile, "");
+ File.WriteAllText(existingClassesFile, "");
+
+ // Setup command executor to create Classes.cs with required content
+ _commandExecutor
+ .ExecuteAsync(Arg.Any(), Arg.Any())
+ .Returns(callInfo =>
+ {
+ var command = callInfo.ArgAt(0);
+ var args = callInfo.ArgAt(1);
+ Console.WriteLine($"Mock received command: {command} {args}");
+
+ if (args.Contains("graphql-codegen"))
+ {
+ // Note the normalized line endings and careful spacing
+ var content = string.Join(
+ Environment.NewLine,
+ "using System;",
+ "using System.Collections.Generic;",
+ "",
+ "namespace Generated",
+ "{",
+ " #region input types",
+ " public class TestInputModel",
+ " {",
+ " public string Id { get; set; }",
+ " }",
+ " #endregion",
+ "",
+ " #region TestQuery",
+ " public class TestQuery",
+ " {",
+ " public string Name { get; set; }",
+ " }",
+ " #endregion",
+ "}"
+ );
+
+ var path = Path.Combine(_tempPath, "Classes.cs");
+ File.WriteAllText(path, content, Encoding.UTF8);
+
+ Console.WriteLine("Created Classes.cs with content:");
+ Console.WriteLine(content);
+ Console.WriteLine($"File exists: {File.Exists(path)}");
+ Console.WriteLine("File content verification:");
+ Console.WriteLine(File.ReadAllText(path));
+ }
+ return Task.CompletedTask;
+ });
+
+ // Act
+ await _generator.GenerateAsync();
+
+ // Output directory contents
+ Console.WriteLine("\nDirectory contents before assertion:");
+ foreach (var file in Directory.GetFiles(_tempPath))
+ {
+ Console.WriteLine($"File: {file}");
+ if (Path.GetFileName(file).EndsWith(".cs"))
+ {
+ Console.WriteLine("Content:");
+ Console.WriteLine(File.ReadAllText(file));
+ }
+ }
+
+ // Assert
+ var modelsPath = Path.Combine(_tempPath, "Models.cs");
+ File.Exists(modelsPath).ShouldBeTrue($"Models.cs should exist at {modelsPath}");
+
+ if (File.Exists(modelsPath))
+ {
+ var content = File.ReadAllText(modelsPath);
+ content.ShouldContain("namespace Test.Namespace");
+ content.ShouldContain("TestInputModel");
+ }
+ }
+
+ [Test]
+ public async Task GenerateAsync_WhenClassesFileNotGenerated_ShouldThrow()
+ {
+ // Arrange
+ SetupMockFiles();
+
+ _commandExecutor
+ .ExecuteAsync(Arg.Any(), Arg.Any())
+ .Returns(Task.CompletedTask);
+
+ // Act & Assert
+ var exception = await Should.ThrowAsync(() => _generator.GenerateAsync());
+ exception.Message.ShouldContain("Generated Classes.cs file not found");
+ }
+
+ [TestCase("using Agoda.CodeGen.GraphQL", "using Agoda.Graphql.Client")]
+ [TestCase("", "")]
+ public async Task GenerateAsync_ShouldReplaceExpectedStrings(string original, string expected)
+ {
+ // Arrange
+ SetupMockFiles();
+
+ _commandExecutor
+ .ExecuteAsync(Arg.Any(), Arg.Any())
+ .Returns(callInfo =>
+ {
+ if (callInfo.ArgAt(1).Contains("graphql-codegen"))
+ {
+ File.WriteAllText(Path.Combine(_tempPath, "Classes.cs"), $@"
+namespace Generated
+{{
+ #region input types
+ {original}
+ public class TestModel {{}}
+ #endregion
+}}");
+ }
+ return Task.CompletedTask;
+ });
+
+ // Act
+ await _generator.GenerateAsync();
+
+ // Assert
+ var generatedContent = File.ReadAllText(Path.Combine(_tempPath, "Models.cs"));
+ generatedContent.ShouldContain(expected);
+ generatedContent.ShouldNotContain(original);
+ }
+
+ private void SetupMockFiles()
+ {
+ var graphqlFile = Path.Combine(_tempPath, "query.graphql");
+ Console.WriteLine($"Creating GraphQL file at {graphqlFile}");
+ File.WriteAllText(graphqlFile, "query { test }");
+ Console.WriteLine($"GraphQL file created: {File.Exists(graphqlFile)}");
+ }
+}
\ No newline at end of file
diff --git a/src/Agoda.GrapqhlGen.sln b/src/Agoda.GrapqhlGen.sln
new file mode 100644
index 0000000..1a7592f
--- /dev/null
+++ b/src/Agoda.GrapqhlGen.sln
@@ -0,0 +1,31 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35327.3
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Agoda.GrapqhlGen", "Agoda.GrapqhlGen\Agoda.GrapqhlGen.csproj", "{CEFF629E-5D42-4245-BC36-25B57627587A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Agoda.GrapqhlGen.Tests", "Agoda.GrapqhlGen.Tests\Agoda.GrapqhlGen.Tests.csproj", "{986253B2-863D-4187-A392-6BA689651F80}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {CEFF629E-5D42-4245-BC36-25B57627587A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CEFF629E-5D42-4245-BC36-25B57627587A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CEFF629E-5D42-4245-BC36-25B57627587A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CEFF629E-5D42-4245-BC36-25B57627587A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {986253B2-863D-4187-A392-6BA689651F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {986253B2-863D-4187-A392-6BA689651F80}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {986253B2-863D-4187-A392-6BA689651F80}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {986253B2-863D-4187-A392-6BA689651F80}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {7800F762-7C35-40B0-A344-A7E2E57FBFE0}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Agoda.GrapqhlGen/Agoda.GrapqhlGen.csproj b/src/Agoda.GrapqhlGen/Agoda.GrapqhlGen.csproj
new file mode 100644
index 0000000..ceb1d0f
--- /dev/null
+++ b/src/Agoda.GrapqhlGen/Agoda.GrapqhlGen.csproj
@@ -0,0 +1,25 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+ true
+ grapqhlgen
+ ./nupkg
+ 1.0.0
+ Agoda.GrapqhlGen
+ Joel Dickson
+ Grapqhl Generation dotnet cli Tool
+
+
+
+
+
+
+
+
+
diff --git a/src/Agoda.GrapqhlGen/CodeGenerator.cs b/src/Agoda.GrapqhlGen/CodeGenerator.cs
new file mode 100644
index 0000000..35541f2
--- /dev/null
+++ b/src/Agoda.GrapqhlGen/CodeGenerator.cs
@@ -0,0 +1,166 @@
+using System.Text;
+using Microsoft.Extensions.Logging;
+
+namespace Agoda.GrapqhlGen;
+
+public class CodeGenerator
+{
+ private readonly string _schemaUrl;
+ private readonly string _inputPath;
+ private readonly string _outputPath;
+ private readonly string _baseNamespace;
+ private readonly Dictionary? _headers;
+ private readonly string _template;
+ private readonly string _modelFile;
+ private readonly ICommandExecutor _commandExecutor;
+ private readonly ILogger _logger;
+
+ public CodeGenerator(
+ string schemaUrl,
+ string inputPath,
+ string outputPath,
+ string baseNamespace,
+ Dictionary? headers,
+ string template,
+ string modelFile,
+ ICommandExecutor? commandExecutor = null,
+ ILogger? logger = null)
+ {
+ _schemaUrl = schemaUrl;
+ _inputPath = inputPath;
+ _outputPath = outputPath;
+ _baseNamespace = baseNamespace;
+ _headers = headers;
+ _template = template;
+ _modelFile = modelFile;
+ _commandExecutor = commandExecutor ?? new CommandExecutor();
+ _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole())
+ .CreateLogger();
+ }
+
+ public async Task GenerateAsync()
+ {
+ _logger.LogInformation("Starting code generation...");
+
+ EnsureDirectoriesExist();
+ CleanWorkingDirectory();
+ CopyGraphQLFiles();
+ await GenerateCode();
+ ProcessRegions();
+ CleanWorkingDirectory();
+
+ _logger.LogInformation("Code generation completed successfully.");
+ }
+
+ private void EnsureDirectoriesExist()
+ {
+ _logger.LogDebug("Ensuring directory exists: {OutputPath}", _outputPath);
+ Directory.CreateDirectory(_outputPath);
+ }
+
+ private void CleanWorkingDirectory()
+ {
+ _logger.LogDebug("Cleaning working directory...");
+ foreach (var file in Directory.GetFiles(_outputPath, "*.graphql"))
+ {
+ _logger.LogDebug("Deleting GraphQL file: {File}", file);
+ File.Delete(file);
+ }
+ var classesFile = Path.Combine(_outputPath, "Classes.cs");
+ if (File.Exists(classesFile))
+ {
+ _logger.LogDebug("Deleting existing Classes.cs: {ClassesFile}", classesFile);
+ File.Delete(classesFile);
+ }
+ }
+
+ private void CopyGraphQLFiles()
+ {
+ _logger.LogDebug("Copying GraphQL files from {InputPath} to {OutputPath}", _inputPath, _outputPath);
+ foreach (var file in Directory.GetFiles(_inputPath, "*.graphql", SearchOption.AllDirectories))
+ {
+ var destFile = Path.Combine(_outputPath, Path.GetFileName(file));
+ _logger.LogDebug("Copying {SourceFile} to {DestFile}", file, destFile);
+ File.Copy(file, destFile, true);
+ }
+ }
+
+ private async Task GenerateCode()
+ {
+ _logger.LogInformation("Starting code generation process...");
+
+ // Ensure pnpm is installed
+ _logger.LogInformation("Installing pnpm...");
+ await _commandExecutor.ExecuteAsync("npm", "install -g pnpm");
+
+ // Install dependencies using pnpm
+ _logger.LogInformation("Installing GraphQL CodeGen dependencies...");
+ await _commandExecutor.ExecuteAsync("pnpm", "install @graphql-codegen/cli @graphql-codegen/typescript");
+
+ // Build pnpm command for code generation
+ var headerArgs = _headers?.Select(h => $"--header \"{h.Key}: {h.Value}\"") ?? Array.Empty();
+ var args = $"graphql-codegen " +
+ $"--schema {_schemaUrl} " +
+ $"--template {_template} " +
+ $"--out {_outputPath} " +
+ string.Join(" ", headerArgs) +
+ $" {Path.Combine(_outputPath, "*.graphql")}";
+
+ _logger.LogDebug("Executing GraphQL CodeGen with args: {Arguments}", args);
+ await _commandExecutor.ExecuteAsync("pnpm", args);
+
+ var classesFile = Path.Combine(_outputPath, "Classes.cs");
+ _logger.LogDebug("Checking for Classes.cs at {ClassesFile}. Exists: {Exists}",
+ classesFile, File.Exists(classesFile));
+ }
+
+ private void ProcessRegions()
+ {
+ var classesFile = Path.Combine(_outputPath, "Classes.cs");
+ _logger.LogDebug("Processing regions from {ClassesFile}", classesFile);
+
+ if (!File.Exists(classesFile))
+ {
+ _logger.LogError("Generated Classes.cs file not found at {ClassesFile}", classesFile);
+ throw new Exception($"Generated Classes.cs file not found at {classesFile}");
+ }
+
+ var regions = RegionParser.Parse(classesFile);
+ _logger.LogDebug("Found {RegionCount} regions", regions.Count());
+
+ // Process models
+ var models = regions.FirstOrDefault(r => r.Name == "input types");
+ if (models != null)
+ {
+ var modelFile = Path.Combine(_outputPath, _modelFile);
+ _logger.LogInformation("Generating models file at {ModelFile}", modelFile);
+ GenerateFile(_modelFile, models);
+ }
+ else
+ {
+ _logger.LogWarning("No input types region found");
+ }
+
+ // Process other regions
+ foreach (var region in regions.Where(r => !new[] { "fragments", "input types", "Query" }.Contains(r.Name)))
+ {
+ var fileName = $"{region.Name}.generated.cs";
+ _logger.LogInformation("Generating file for region {RegionName} at {FileName}", region.Name, fileName);
+ GenerateFile(fileName, region);
+ }
+ }
+
+ private void GenerateFile(string fileName, Region region)
+ {
+ var filePath = Path.Combine(_outputPath, fileName);
+ _logger.LogDebug("Generating file at {FilePath}", filePath);
+
+ var content = region.GetSourceCode()
+ .Replace("namespace Generated", $"\nnamespace {_baseNamespace}")
+ .Replace("using Agoda.CodeGen.GraphQL", "using Agoda.Graphql.Client")
+ .Replace("", "");
+
+ File.WriteAllText(filePath, content, Encoding.UTF8);
+ _logger.LogDebug("File generated successfully at {FilePath}", filePath);
+ }
+}
\ No newline at end of file
diff --git a/src/Agoda.GrapqhlGen/CommandExecutor.cs b/src/Agoda.GrapqhlGen/CommandExecutor.cs
new file mode 100644
index 0000000..cc51cbc
--- /dev/null
+++ b/src/Agoda.GrapqhlGen/CommandExecutor.cs
@@ -0,0 +1,58 @@
+using Microsoft.Extensions.Logging;
+
+namespace Agoda.GrapqhlGen;
+
+public interface ICommandExecutor
+{
+ Task ExecuteAsync(string command, string arguments);
+}
+
+public class CommandExecutor : ICommandExecutor
+{
+ private readonly ILogger _logger;
+
+ public CommandExecutor(ILogger? logger = null)
+ {
+ _logger = logger ?? LoggerFactory.Create(builder => builder.AddConsole())
+ .CreateLogger();
+ }
+
+ public async Task ExecuteAsync(string command, string arguments)
+ {
+ var processStartInfo = new System.Diagnostics.ProcessStartInfo
+ {
+ FileName = command,
+ Arguments = arguments,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ _logger.LogDebug("Executing command: {Command} {Arguments}", command, arguments);
+
+ using var process = System.Diagnostics.Process.Start(processStartInfo);
+ if (process == null)
+ {
+ _logger.LogError("Failed to start process: {Command}", command);
+ throw new Exception($"Failed to start process: {command}");
+ }
+
+ var output = await process.StandardOutput.ReadToEndAsync();
+ var error = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+
+ if (process.ExitCode != 0)
+ {
+ _logger.LogError("Command failed with exit code {ExitCode}. Error: {Error}",
+ process.ExitCode, error);
+ throw new Exception($"Command failed with exit code {process.ExitCode}. Error: {error}");
+ }
+
+ if (!string.IsNullOrWhiteSpace(output))
+ {
+ _logger.LogDebug("Command output: {Output}", output);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Agoda.GrapqhlGen/Program.cs b/src/Agoda.GrapqhlGen/Program.cs
new file mode 100644
index 0000000..75de3b6
--- /dev/null
+++ b/src/Agoda.GrapqhlGen/Program.cs
@@ -0,0 +1,131 @@
+using System.CommandLine;
+using System.Text;
+using Agoda.GrapqhlGen;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+
+namespace GraphqlCodeGen;
+
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var rootCommand = new RootCommand("GraphQL Code Generation Tool");
+
+ // Required options
+ var schemaUrlOption = new Option(
+ name: "--schema-url",
+ description: "URL of the GraphQL schema")
+ { IsRequired = true };
+
+ var inputPathOption = new Option(
+ name: "--input-path",
+ description: "Path to the directory containing .graphql files")
+ { IsRequired = true };
+
+ var outputPathOption = new Option(
+ name: "--output-path",
+ description: "Path where generated files will be saved")
+ { IsRequired = true };
+
+ // Optional options
+ var namespaceOption = new Option(
+ name: "--namespace",
+ description: "Base namespace for generated code",
+ getDefaultValue: () => "Generated");
+
+ var headersOption = new Option(
+ name: "--headers",
+ description: "Headers to include in the schema request (format: 'Key: Value')")
+ { AllowMultipleArgumentsPerToken = true };
+
+ var templateOption = new Option(
+ name: "--template",
+ description: "Template to use for code generation",
+ getDefaultValue: () => "typescript");
+
+ var modelFileOption = new Option(
+ name: "--model-file",
+ description: "Name of the generated models file",
+ getDefaultValue: () => "Models.cs");
+
+ var logLevelOption = new Option(
+ name: "--log-level",
+ description: "Set the logging level (Debug, Information, Warning, Error, Critical)",
+ getDefaultValue: () => LogLevel.Information);
+
+ // Add options to command
+ rootCommand.AddOption(schemaUrlOption);
+ rootCommand.AddOption(inputPathOption);
+ rootCommand.AddOption(outputPathOption);
+ rootCommand.AddOption(namespaceOption);
+ rootCommand.AddOption(headersOption);
+ rootCommand.AddOption(templateOption);
+ rootCommand.AddOption(modelFileOption);
+ rootCommand.AddOption(logLevelOption);
+
+ rootCommand.SetHandler(async (
+ string schemaUrl,
+ string inputPath,
+ string outputPath,
+ string namespaceName,
+ string[] headers,
+ string template,
+ string modelFile,
+ LogLevel logLevel) =>
+ {
+ try
+ {
+ var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder
+ .SetMinimumLevel(logLevel)
+ .AddConsole(options =>
+ {
+ options.FormatterName = ConsoleFormatterNames.Simple;
+ });
+ });
+
+ var logger = loggerFactory.CreateLogger();
+
+ var generator = new CodeGenerator(
+ schemaUrl,
+ inputPath,
+ outputPath,
+ namespaceName,
+ headers?.ToDictionary(h => h.Split(':')[0].Trim(), h => h.Split(':')[1].Trim()),
+ template,
+ modelFile,
+ commandExecutor: null,
+ logger: logger);
+
+ await generator.GenerateAsync();
+ }
+ catch (Exception ex)
+ {
+ using var loggerFactory = LoggerFactory.Create(builder =>
+ {
+ builder
+ .SetMinimumLevel(LogLevel.Error)
+ .AddConsole(options =>
+ {
+ options.FormatterName = ConsoleFormatterNames.Simple;
+ });
+ });
+ var logger = loggerFactory.CreateLogger();
+ logger.LogError(ex, "Error during code generation");
+ Environment.Exit(1);
+ }
+ },
+ schemaUrlOption,
+ inputPathOption,
+ outputPathOption,
+ namespaceOption,
+ headersOption,
+ templateOption,
+ modelFileOption,
+ logLevelOption);
+
+ return await rootCommand.InvokeAsync(args);
+ }
+}
\ No newline at end of file
diff --git a/src/Agoda.GrapqhlGen/Region.cs b/src/Agoda.GrapqhlGen/Region.cs
new file mode 100644
index 0000000..eab05c7
--- /dev/null
+++ b/src/Agoda.GrapqhlGen/Region.cs
@@ -0,0 +1,7 @@
+namespace Agoda.GrapqhlGen;
+
+public record Region(string Name, List Lines)
+{
+ public string GetSourceCode() =>
+ string.Join(Environment.NewLine, Lines.Where(l => !string.IsNullOrWhiteSpace(l)));
+}
diff --git a/src/Agoda.GrapqhlGen/RegionParser.cs b/src/Agoda.GrapqhlGen/RegionParser.cs
new file mode 100644
index 0000000..931aa7f
--- /dev/null
+++ b/src/Agoda.GrapqhlGen/RegionParser.cs
@@ -0,0 +1,78 @@
+using Microsoft.Extensions.Logging;
+
+namespace Agoda.GrapqhlGen;
+
+public static class RegionParser
+{
+ public static IEnumerable Parse(string filePath, ILogger? logger = null)
+ {
+ logger ??= LoggerFactory.Create(builder => builder.AddConsole())
+ .CreateLogger(typeof(RegionParser).FullName!);
+
+ if (!File.Exists(filePath))
+ {
+ logger.LogError("File does not exist: {FilePath}", filePath);
+ return Enumerable.Empty();
+ }
+
+ var fileContent = File.ReadAllText(filePath);
+ logger.LogDebug("File content before processing:{NewLine}{FileContent}",
+ Environment.NewLine, fileContent);
+
+ var regions = new List();
+ var lines = File.ReadAllLines(filePath);
+ var currentRegion = "";
+ var currentLines = new List();
+ var imports = new List();
+ var inRegion = false;
+
+ foreach (var line in lines)
+ {
+ logger.LogTrace("Processing line: '{Line}'", line);
+
+ if (line.TrimStart().StartsWith("#region"))
+ {
+ inRegion = true;
+ currentRegion = line.TrimStart().Replace("#region", "").Trim();
+ logger.LogDebug("Starting region: '{RegionName}'", currentRegion);
+ continue;
+ }
+
+ if (line.TrimStart().StartsWith("#endregion"))
+ {
+ if (inRegion)
+ {
+ logger.LogDebug("Ending region: '{RegionName}' with {LineCount} lines",
+ currentRegion, currentLines.Count);
+ regions.Add(new Region(currentRegion, imports.Concat(currentLines).ToList()));
+ currentLines = new List();
+ inRegion = false;
+ }
+ continue;
+ }
+
+ if (!inRegion)
+ {
+ imports.Add(line);
+ }
+ else
+ {
+ currentLines.Add(line);
+ }
+ }
+
+ logger.LogInformation("Found {RegionCount} regions", regions.Count);
+
+ foreach (var region in regions)
+ {
+ logger.LogDebug(
+ "Region: '{RegionName}' with {LineCount} lines{NewLine}Content:{NewLine}{Content}",
+ region.Name,
+ region.Lines.Count,
+ Environment.NewLine,
+ region.GetSourceCode());
+ }
+
+ return regions;
+ }
+}
\ No newline at end of file