< Summary

Information
Class: NGql.Tool.RenderCommand
Assembly: NGql.Tool
File(s): /home/runner/work/NGql/NGql/tools/Tool/RenderCommand.cs
Line coverage
100%
Covered lines: 80
Uncovered lines: 0
Coverable lines: 80
Total lines: 192
Line coverage: 100%
Branch coverage
100%
Covered branches: 41
Total branches: 41
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Input()100%11100%
get_Execute()100%11100%
get_Endpoint()100%11100%
get_Headers()100%11100%
get_Variables()100%11100%
get_AllowMutations()100%11100%
ExecuteAsync()100%2424100%
TryParseHeaders(...)100%44100%
TryParseVariables(...)100%44100%
ParseVariableValue(...)100%11100%
JsonElementToObject(...)100%99100%

File(s)

/home/runner/work/NGql/NGql/tools/Tool/RenderCommand.cs

#LineLine coverage
 1using System.ComponentModel;
 2using System.Text.Json;
 3
 4using Spectre.Console.Cli;
 5
 6namespace NGql.Tool;
 7
 8/// <summary>
 9/// Default command. Renders an NGql snippet as GraphQL; optionally posts to a live endpoint.
 10/// </summary>
 11internal sealed class RenderCommand : AsyncCommand<RenderCommand.Settings>
 12{
 13    /// <summary>
 14    /// Stand-in for the bare "-" argument, which Spectre's parser rejects before the command sees
 15    /// it. <see cref="Program"/> substitutes this; it is not part of the documented CLI surface.
 16    /// </summary>
 17    public const string StdInMarker = "\u0000stdin";
 18
 19    public sealed class Settings : CommandSettings
 20    {
 21        [CommandArgument(0, "[INPUT]")]
 22        [Description("Path to a snippet file, or '-' for stdin (default if no value provided).")]
 48323        public string? Input { get; init; }
 24
 25        [CommandOption("--execute")]
 26        [Description("After rendering, POST the operation to --endpoint and print the response.")]
 15927        public bool Execute { get; init; }
 28
 29        [CommandOption("--endpoint <URL>")]
 30        [Description("GraphQL endpoint URL to POST the rendered operation to. Required with --execute.")]
 15631        public string? Endpoint { get; init; }
 32
 33        [CommandOption("-H|--header <HEADER>")]
 34        [Description("HTTP header in 'Name: value' form. Repeatable.")]
 14735        public string[] Headers { get; init; } = [];
 36
 37        [CommandOption("--var <KEY_VALUE>")]
 38        [Description("GraphQL variable in 'key=value' form. Values are JSON-parsed when possible (numbers, bools, arrays
 14139        public string[] Variables { get; init; } = [];
 40
 41        [CommandOption("--allow-mutations")]
 42        [Description("Opt in to executing mutations. Default refuses for safety; mutations have side effects.")]
 9343        public bool AllowMutations { get; init; }
 44    }
 45
 46    public override async Task<int> ExecuteAsync(CommandContext context, Settings s)
 47    {
 48        // ── 1. Resolve snippet text ────────────────────────────────────────────────────────
 49        string snippet;
 50        string sourceLabel;
 8751        if (s.Input is null || s.Input == StdInMarker)
 52        {
 1253            if (s.Input is null && !ToolEnvironment.IsInputRedirected())
 54            {
 355                await Console.Error.WriteLineAsync("ngql: no input. Pipe a snippet on stdin or pass a file path. See `ng
 356                return ExitCodes.InvalidUsage;
 57            }
 58
 959            snippet = await Console.In.ReadToEndAsync();
 960            sourceLabel = "<stdin>";
 61        }
 62        else
 63        {
 7564            if (!File.Exists(s.Input))
 65            {
 366                await Console.Error.WriteLineAsync($"ngql: file not found: {s.Input}");
 367                return ExitCodes.FileNotFound;
 68            }
 69
 7270            snippet = await File.ReadAllTextAsync(s.Input);
 7271            sourceLabel = s.Input;
 72        }
 73
 74        // ── 2. Compile + render ────────────────────────────────────────────────────────────
 8175        var result = await SnippetRunner.CompileAndRun(snippet);
 8176        if (!result.Ok)
 77        {
 978            await Console.Error.WriteLineAsync($"ngql: failed to render {sourceLabel}");
 979            await Console.Error.WriteLineAsync(result.Error);
 980            return ExitCodes.RenderFailed;
 81        }
 82
 7283        var graphql = result.Output;
 84
 85        // ── 3. Render-only path ────────────────────────────────────────────────────────────
 7286        if (!s.Execute)
 87        {
 1288            Console.WriteLine(graphql);
 1289            return ExitCodes.Ok;
 90        }
 91
 92        // ── 4. Execute path ────────────────────────────────────────────────────────────────
 6093        if (string.IsNullOrWhiteSpace(s.Endpoint))
 94        {
 695            await Console.Error.WriteLineAsync("ngql: --execute requires --endpoint <url>.");
 696            return ExitCodes.InvalidUsage;
 97        }
 98
 5499        if (SnippetRunner.IsMutation(graphql) && !s.AllowMutations)
 100        {
 3101            await Console.Error.WriteLineAsync("ngql: refusing to execute a mutation by default. Re-run with --allow-mut
 3102            await Console.Error.WriteLineAsync("(Rendered operation:)");
 3103            await Console.Error.WriteLineAsync(graphql);
 3104            return ExitCodes.MutationBlocked;
 105        }
 106
 51107        if (!TryParseHeaders(s.Headers, out var headers, out var headerError))
 108        {
 6109            await Console.Error.WriteLineAsync($"ngql: {headerError}");
 6110            return ExitCodes.InvalidUsage;
 111        }
 112
 45113        if (!TryParseVariables(s.Variables, out var variables, out var varError))
 114        {
 6115            await Console.Error.WriteLineAsync($"ngql: {varError}");
 6116            return ExitCodes.InvalidUsage;
 117        }
 118
 39119        return await Executor.ExecuteAsync(graphql, s.Endpoint, headers, variables);
 87120    }
 121
 122    private static bool TryParseHeaders(
 123        string[] raw,
 124        out List<KeyValuePair<string, string>> result,
 125        out string? error)
 126    {
 51127        result = new List<KeyValuePair<string, string>>(raw.Length);
 120128        foreach (var entry in raw)
 129        {
 12130            var colon = entry.IndexOf(':');
 12131            if (colon <= 0)
 132            {
 6133                error = $"header must be in 'Name: value' form, got '{entry}'";
 6134                return false;
 135            }
 6136            result.Add(new KeyValuePair<string, string>(
 6137                entry[..colon].Trim(),
 6138                entry[(colon + 1)..].Trim()));
 139        }
 45140        error = null;
 45141        return true;
 142    }
 143
 144    private static bool TryParseVariables(
 145        string[] raw,
 146        out Dictionary<string, object?> result,
 147        out string? error)
 148    {
 45149        result = new Dictionary<string, object?>(StringComparer.Ordinal);
 150150        foreach (var entry in raw)
 151        {
 33152            var eq = entry.IndexOf('=');
 33153            if (eq <= 0)
 154            {
 6155                error = $"variable must be in 'key=value' form, got '{entry}'";
 6156                return false;
 157            }
 27158            var key = entry[..eq];
 27159            var rawValue = entry[(eq + 1)..];
 27160            result[key] = ParseVariableValue(rawValue);
 161        }
 39162        error = null;
 39163        return true;
 164    }
 165
 166    private static object? ParseVariableValue(string raw)
 167    {
 168        // JSON-parse if possible (catches numbers, bools, arrays, objects, null, quoted strings).
 169        // Fall back to bare-string semantics so `--var name=alice` works without quoting.
 170        try
 171        {
 27172            using var doc = JsonDocument.Parse(raw);
 24173            return JsonElementToObject(doc.RootElement);
 174        }
 3175        catch (JsonException)
 176        {
 3177            return raw;
 178        }
 27179    }
 180
 33181    private static object? JsonElementToObject(JsonElement el) => el.ValueKind switch
 33182    {
 9183        JsonValueKind.Object => el.EnumerateObject().ToDictionary(p => p.Name, p => JsonElementToObject(p.Value)),
 3184        JsonValueKind.Array => el.EnumerateArray().Select(JsonElementToObject).ToList(),
 9185        JsonValueKind.String => el.GetString(),
 9186        JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(),
 3187        JsonValueKind.True => true,
 3188        JsonValueKind.False => false,
 33189        // Remaining kinds are Null and Undefined; JsonDocument.Parse never yields Undefined.
 3190        _ => null,
 33191    };
 192}