Skip to content

Commit 6285e5a

Browse files
committed
Add get command, fix issue with output not truncating the file and not being able to output to a new file
1 parent 687b492 commit 6285e5a

File tree

11 files changed

+173
-31
lines changed

11 files changed

+173
-31
lines changed

dotnet-json.Tests/FileArgumentTests.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System;
21
using System.CommandLine;
32
using System.CommandLine.Invocation;
43
using System.CommandLine.IO;
@@ -27,7 +26,7 @@ public async Task SucceedsWhenFileExists()
2726

2827
exitCode.Should().Be(0);
2928
console.Error.ToString().Should().BeEmpty();
30-
console.Out.ToString().Should().Contain($"Success {filename}");
29+
console.Out.ToString().Should().Contain("Success");
3130
}
3231
finally
3332
{
@@ -42,7 +41,7 @@ public async Task SucceedsWithStandardInputOutput()
4241

4342
exitCode.Should().Be(0);
4443
console.Error.ToString().Should().BeEmpty();
45-
console.Out.ToString().Should().Contain("Success -");
44+
console.Out.ToString().Should().Contain("Success");
4645
}
4746

4847
[Fact]
@@ -54,9 +53,20 @@ public async Task ThrowsWhenFileDoesNotExist()
5453
console.Error.ToString().Should().Contain("File does not exist: this-file-does-not-exist.json");
5554
}
5655

57-
private async Task<(int ExitCode, IConsole Console)> RunCommand(string filename)
56+
[Fact]
57+
public async Task DoesNotThrowOnNonExistingFileIfAllowNewFileIsTrue()
58+
{
59+
var (exitCode, console) = await RunCommand("this-file-does-not-exist.json", allowNewFile: true);
60+
61+
exitCode.Should().Be(0);
62+
console.Error.ToString().Should().BeEmpty();
63+
console.Out.ToString().Should().Contain("Success");
64+
}
65+
66+
private async Task<(int ExitCode, IConsole Console)> RunCommand(string filename, bool allowNewFile = false)
5867
{
5968
var file = new FileArgument("file");
69+
file.AllowNewFile = allowNewFile;
6070

6171
var command = new RootCommand();
6272
command.AddArgument(file);

dotnet-json.Tests/IntegrationTests.cs

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.CommandLine;
3+
using System.CommandLine.IO;
24
using System.IO;
35
using System.Threading.Tasks;
46
using FluentAssertions;
@@ -26,14 +28,16 @@ public async Task Set()
2628
{
2729
await File.WriteAllTextAsync(Path.Join(_tmpDir, "set.json"), @"{ ""key"": ""value"" }");
2830

29-
await Program.Main(new[]
31+
var (exitCode, console) = await RunCommand(new[]
3032
{
3133
"set",
3234
Path.Join(_tmpDir, "set.json"),
3335
"path:to:0:key",
3436
"value",
3537
});
3638

39+
exitCode.Should().Be(0);
40+
3741
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "set.json"));
3842
content.Should().Be(@"{
3943
""key"": ""value"",
@@ -52,13 +56,15 @@ public async Task Remove()
5256
{
5357
await File.WriteAllTextAsync(Path.Join(_tmpDir, "remove.json"), @"{ ""key"": ""value"", ""path"": { ""to"": [ { ""key"": ""value"" } ] } }");
5458

55-
await Program.Main(new[]
59+
var (exitCode, console) = await RunCommand(new[]
5660
{
5761
"remove",
5862
Path.Join(_tmpDir, "remove.json"),
5963
"path:to:0:key",
6064
});
6165

66+
exitCode.Should().Be(0);
67+
6268
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "remove.json"));
6369
content.Should().Be(@"{
6470
""key"": ""value"",
@@ -77,15 +83,19 @@ public async Task Merge()
7783
await File.WriteAllTextAsync(Path.Join(_tmpDir, "b.json"), @"{ ""b"": { ""key"": ""value"" } }");
7884
await File.WriteAllTextAsync(Path.Join(_tmpDir, "c.json"), @"{ ""c"": [ 1 ] }");
7985

80-
await Program.Main(new[]
86+
var (exitCode, console) = await RunCommand(new[]
8187
{
8288
"merge",
8389
Path.Join(_tmpDir, "a.json"),
8490
Path.Join(_tmpDir, "b.json"),
8591
Path.Join(_tmpDir, "c.json"),
92+
"-o",
93+
Path.Join(_tmpDir, "d.json"),
8694
});
8795

88-
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "a.json"));
96+
exitCode.Should().Be(0);
97+
98+
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "d.json"));
8999
content.Should().Be(@"{
90100
""b"": {
91101
""key"": ""value""
@@ -95,5 +105,50 @@ await Program.Main(new[]
95105
]
96106
}");
97107
}
108+
109+
[Fact]
110+
public async Task Merge_DoesNotLeaveTraceOfPreviousJsonInFile()
111+
{
112+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "a.json"), @"{
113+
// This file uses comments
114+
""b"": {
115+
// To have more lines of JSON
116+
// then the resulting file
117+
""key"": ""value""
118+
}
119+
// So to test that it does not leave behind
120+
// data from the previous file and it still
121+
// is a valid JSON file after merge
122+
}");
123+
await File.WriteAllTextAsync(Path.Join(_tmpDir, "b.json"), @"{ ""a"": 1 }");
124+
125+
var (exitCode, console) = await RunCommand(new[]
126+
{
127+
"merge",
128+
Path.Join(_tmpDir, "a.json"),
129+
Path.Join(_tmpDir, "b.json"),
130+
});
131+
132+
exitCode.Should().Be(0);
133+
134+
var content = await File.ReadAllTextAsync(Path.Join(_tmpDir, "a.json"));
135+
content.Should().Be(@"{
136+
""b"": {
137+
""key"": ""value""
138+
},
139+
""a"": 1
140+
}");
141+
}
142+
143+
private async Task<(int ExitCode, IConsole Console)> RunCommand(string[] args)
144+
{
145+
var rootCommand = Program.CreateRootCommand();
146+
147+
var console = new TestConsole();
148+
149+
var exitCode = await rootCommand.InvokeAsync(args, console);
150+
151+
return (exitCode, console);
152+
}
98153
}
99154
}

dotnet-json.Tests/dotnet-json.Tests.csproj

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,16 @@
1111

1212
<ItemGroup>
1313
<PackageReference Include="FluentAssertions" Version="6.0.0-alpha0001" />
14-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
15-
<PackageReference Include="xunit" Version="2.4.0" />
16-
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
17-
<PackageReference Include="coverlet.collector" Version="1.2.0" />
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
15+
<PackageReference Include="xunit" Version="2.4.1" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
17+
<PrivateAssets>all</PrivateAssets>
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
</PackageReference>
20+
<PackageReference Include="coverlet.collector" Version="1.3.0">
21+
<PrivateAssets>all</PrivateAssets>
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23+
</PackageReference>
1824
</ItemGroup>
1925

2026
<ItemGroup>

dotnet-json/Commands/CommandBase.cs

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,33 @@ public abstract class CommandBase : Command, ICommandHandler
1414
{
1515
protected FileArgument InputFile = new FileArgument("file", "The JSON file (use '-' for STDIN)");
1616

17-
protected FileOption OutputFile = new FileOption(new[] { "-o", "--output" }, "The output file (use '-' for STDOUT, defaults to <file>)") { Argument = { Name = "file", Arity = ArgumentArity.ZeroOrOne } };
17+
protected FileOption OutputFile = new FileOption(new[] { "-o", "--output" }, "The output file (use '-' for STDOUT, defaults to <file>)") { AllowNewFile = true };
1818

1919
protected Option<bool> Compressed = new Option<bool>(new[] { "-c", "--compressed" }, "Write the output in compressed form (defaults to indented)");
2020

21-
private InvocationContext? _context = null;
21+
protected InvocationContext? Context = null;
2222

23-
protected CommandBase(string name, string? description = null)
23+
protected CommandBase(string name, string? description = null, bool includeOutputOption = true)
2424
: base(name, description)
2525
{
2626
AddArgument(InputFile);
27-
AddOption(OutputFile);
27+
28+
if (includeOutputOption)
29+
AddOption(OutputFile);
2830

2931
Handler = this;
3032
}
3133

3234
public Task<int> InvokeAsync(InvocationContext context)
3335
{
34-
_context = context;
36+
Context = context;
3537

3638
return ExecuteAsync();
3739
}
3840

3941
protected Stream GetInputStream()
4042
{
41-
var filename = _context?.ParseResult.ValueForArgument(InputFile) ?? throw new Exception("GetInputStream must be called from a command handler");
43+
var filename = Context?.ParseResult.ValueForArgument(InputFile) ?? throw new Exception("GetInputStream must be called from a command handler");
4244

4345
return filename switch
4446
{
@@ -49,34 +51,34 @@ protected Stream GetInputStream()
4951

5052
protected Stream GetOutputStream()
5153
{
52-
var filename = _context?.ParseResult.HasOption(OutputFile) ?? throw new Exception("GetOutputStream() must be called from a command handler")
53-
? _context.ParseResult.ValueForOption(OutputFile)
54-
: _context.ParseResult.ValueForArgument(InputFile);
54+
var filename = Context?.ParseResult.HasOption(OutputFile) ?? throw new Exception("GetOutputStream() must be called from a command handler")
55+
? Context.ParseResult.ValueForOption(OutputFile)
56+
: Context.ParseResult.ValueForArgument(InputFile);
5557

5658
return filename switch
5759
{
5860
"-" => Console.OpenStandardOutput(),
59-
_ => File.OpenWrite(filename),
61+
_ => File.Create(filename),
6062
};
6163
}
6264

6365
protected Formatting GetFormatting()
6466
{
65-
return _context?.ParseResult.HasOption(Compressed) ?? throw new Exception("GetFormatting() must be called from a command handler")
67+
return Context?.ParseResult.HasOption(Compressed) ?? throw new Exception("GetFormatting() must be called from a command handler")
6668
? Formatting.None
6769
: Formatting.Indented;
6870
}
6971

7072
[return: MaybeNull]
7173
protected T GetParameterValue<T>(Argument<T> argument)
7274
{
73-
return (_context ?? throw new Exception("GetParameterValue() must be called from a command handler"))
75+
return (Context ?? throw new Exception("GetParameterValue() must be called from a command handler"))
7476
.ParseResult.ValueForArgument(argument);
7577
}
7678

7779
protected List<T>? GetMultiParameterValue<T>(Argument<T> argument)
7880
{
79-
return (_context ?? throw new Exception("GetMultiParameterValue() must be called from a command handler"))
81+
return (Context ?? throw new Exception("GetMultiParameterValue() must be called from a command handler"))
8082
.ParseResult.ValueForArgument<List<T>>(argument);
8183
}
8284

dotnet-json/Commands/FileArgument.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@ public FileArgument(string name, string? description = null)
1717
AddValidator();
1818
}
1919

20+
public bool AllowNewFile { get; set; }
21+
2022
private void AddValidator()
2123
{
2224
this.AddValidator(symbol =>
2325
symbol.Tokens
2426
.Select(t => t.Value)
27+
.Where(_ => !AllowNewFile) // Need to check AllowNewFile at this point because AddValidator() is called from constructor
2528
.Where(filePath => filePath != "-")
2629
.Where(filePath => !File.Exists(filePath))
2730
.Select(filePath => $"File does not exist: {filePath}")

dotnet-json/Commands/FileOption.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ public class FileOption : Option<string>
1010
public FileOption(string alias, string? description = null)
1111
: base(alias, description)
1212
{
13-
base.Argument = new FileArgument();
13+
base.Argument = new FileArgument("file") { Arity = ArgumentArity.ExactlyOne };
1414
_initialized = true;
1515
}
1616

1717
public FileOption(string[] aliases, string? description = null)
1818
: base(aliases, description)
1919
{
20-
base.Argument = new FileArgument();
20+
base.Argument = new FileArgument("file") { Arity = ArgumentArity.ExactlyOne };
2121
_initialized = true;
2222
}
2323

24+
public bool AllowNewFile
25+
{
26+
get => ((FileArgument)Argument).AllowNewFile;
27+
set => ((FileArgument)Argument).AllowNewFile = value;
28+
}
29+
2430
public override Argument Argument
2531
{
2632
set

dotnet-json/Commands/GetCommand.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System;
2+
using System.CommandLine;
3+
using System.Threading.Tasks;
4+
using dotnet_json.Core;
5+
using Newtonsoft.Json.Linq;
6+
7+
namespace dotnet_json.Commands
8+
{
9+
public class GetCommand : CommandBase
10+
{
11+
private Argument<string> Key = new Argument<string>("key", "The key to get (use ':' to get a nested object and use index numbers to get array values eg. nested:key or nested:1:key)") { Arity = ArgumentArity.ExactlyOne };
12+
13+
private Option<bool> Exact = new Option<bool>(new[] { "-e", "--exact" }, "only return exact value matches, this will return an error for references to nested objects/arrays.");
14+
15+
public GetCommand()
16+
: base("get", "Read a value from a JSON file.", false)
17+
{
18+
AddArgument(Key);
19+
AddOption(Exact);
20+
}
21+
22+
protected override async Task<int> ExecuteAsync()
23+
{
24+
var key = GetParameterValue(Key) ?? throw new ArgumentException("Missing argument <key>");
25+
26+
JsonDocument document;
27+
28+
await using (var inputStream = GetInputStream())
29+
document = JsonDocument.ReadFromStream(inputStream);
30+
31+
var result = document[key];
32+
if (result == null)
33+
{
34+
Console.Error.WriteLine($"Key '{key}' does not exist in the json");
35+
return 1;
36+
}
37+
38+
if (Context!.ParseResult.ValueForOption(Exact) && !(result is JValue))
39+
{
40+
Console.Error.WriteLine($"Value for key '{key}' is a complex object.");
41+
return 1;
42+
}
43+
44+
Console.WriteLine(ToString(result));
45+
return 0;
46+
}
47+
48+
private static string ToString(object obj) => obj switch
49+
{
50+
null => "null",
51+
JValue value when value.Value is null => "null",
52+
JValue value => ToString(value.Value!),
53+
bool b => b.ToString().ToLowerInvariant(),
54+
_ => obj.ToString() ?? "null",
55+
};
56+
}
57+
}

dotnet-json/Commands/MergeCommand.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
using System;
22
using System.CommandLine;
3-
using System.CommandLine.Invocation;
43
using System.IO;
54
using System.Threading.Tasks;
65
using dotnet_json.Core;
76

87
namespace dotnet_json.Commands
98
{
10-
public class MergeCommand : CommandBase, ICommandHandler
9+
public class MergeCommand : CommandBase
1110
{
1211
private FileArgument Files = new FileArgument("files", "The names of the files to merge with the first file.") { Arity = ArgumentArity.OneOrMore };
1312

dotnet-json/Commands/RemoveCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public RemoveCommand()
1515
{
1616
AddArgument(Key);
1717

18+
AddAlias("rm");
19+
1820
Handler = this;
1921
}
2022

dotnet-json/Core/JsonDocument.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ public void Merge(JsonDocument document)
3737
SetValue(key, ToValue(value));
3838
}
3939

40-
public object this[string key]
40+
public object? this[string key]
4141
{
42+
get => FindToken(key);
4243
set => SetValue(key, value);
4344
}
4445

0 commit comments

Comments
 (0)