Skip to content

Commit ebce5b1

Browse files
cursoragentjxnkwlp
andcommitted
Add net9/net10 targets, fix serialization bugs, and add unit tests
- Multi-target library: netstandard2.0/2.1 + net9.0 + net10.0 - Upgrade demo and new xUnit project to net10.0; CI uses .NET 10 - Preserve token order on write; default Save to UTF-8 without BOM - Fix GetTokensFromPath NRE, Remove index handling, parser errors Co-authored-by: Passingwind <jxnkwlp@users.noreply.github.com>
1 parent 1861805 commit ebce5b1

16 files changed

Lines changed: 853 additions & 66 deletions

.github/workflows/build.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@ jobs:
1515
- name: Setup .NET
1616
uses: actions/setup-dotnet@v4
1717
with:
18-
dotnet-version: 8.0.x
18+
dotnet-version: 10.0.x
1919

2020
- name: Restore dependencies
2121
run: dotnet restore
2222

2323
- name: Build
2424
run: dotnet build --no-restore -c Release
25+
26+
- name: Test
27+
run: dotnet test --no-build -c Release --verbosity normal

.github/workflows/pack.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- name: Setup .NET
1717
uses: actions/setup-dotnet@v4
1818
with:
19-
dotnet-version: 8.0.x
19+
dotnet-version: 10.0.x
2020

2121
- name: Install Dotnet Tool
2222
run: |

NginxConfigParser.sln

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,54 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1313
README.md = README.md
1414
EndProjectSection
1515
EndProject
16+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NginxConfigParserUnitTests", "NginxConfigParserUnitTests\NginxConfigParserUnitTests.csproj", "{A16D4B71-0B10-4355-8445-22A47A7DA945}"
17+
EndProject
1618
Global
1719
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1820
Debug|Any CPU = Debug|Any CPU
21+
Debug|x64 = Debug|x64
22+
Debug|x86 = Debug|x86
1923
Release|Any CPU = Release|Any CPU
24+
Release|x64 = Release|x64
25+
Release|x86 = Release|x86
2026
EndGlobalSection
2127
GlobalSection(ProjectConfigurationPlatforms) = postSolution
2228
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2329
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Debug|Any CPU.Build.0 = Debug|Any CPU
30+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Debug|x64.ActiveCfg = Debug|Any CPU
31+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Debug|x64.Build.0 = Debug|Any CPU
32+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Debug|x86.ActiveCfg = Debug|Any CPU
33+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Debug|x86.Build.0 = Debug|Any CPU
2434
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Release|Any CPU.ActiveCfg = Release|Any CPU
2535
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Release|Any CPU.Build.0 = Release|Any CPU
36+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Release|x64.ActiveCfg = Release|Any CPU
37+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Release|x64.Build.0 = Release|Any CPU
38+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Release|x86.ActiveCfg = Release|Any CPU
39+
{ED1C5B3D-DE74-4A13-9147-D549A9F5B38E}.Release|x86.Build.0 = Release|Any CPU
2640
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
2741
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Debug|Any CPU.Build.0 = Debug|Any CPU
42+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Debug|x64.ActiveCfg = Debug|Any CPU
43+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Debug|x64.Build.0 = Debug|Any CPU
44+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Debug|x86.ActiveCfg = Debug|Any CPU
45+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Debug|x86.Build.0 = Debug|Any CPU
2846
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Release|Any CPU.ActiveCfg = Release|Any CPU
2947
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Release|Any CPU.Build.0 = Release|Any CPU
48+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Release|x64.ActiveCfg = Release|Any CPU
49+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Release|x64.Build.0 = Release|Any CPU
50+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Release|x86.ActiveCfg = Release|Any CPU
51+
{2BBCF05E-1BE5-47E7-A314-1C7D6591704D}.Release|x86.Build.0 = Release|Any CPU
52+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Debug|Any CPU.Build.0 = Debug|Any CPU
54+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Debug|x64.ActiveCfg = Debug|Any CPU
55+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Debug|x64.Build.0 = Debug|Any CPU
56+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Debug|x86.ActiveCfg = Debug|Any CPU
57+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Debug|x86.Build.0 = Debug|Any CPU
58+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Release|Any CPU.Build.0 = Release|Any CPU
60+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Release|x64.ActiveCfg = Release|Any CPU
61+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Release|x64.Build.0 = Release|Any CPU
62+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Release|x86.ActiveCfg = Release|Any CPU
63+
{A16D4B71-0B10-4355-8445-22A47A7DA945}.Release|x86.Build.0 = Release|Any CPU
3064
EndGlobalSection
3165
GlobalSection(SolutionProperties) = preSolution
3266
HideSolutionNode = FALSE

NginxConfigParser/NginxConfig.cs

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ namespace NginxConfigParser;
1313
public class NginxConfig
1414
{
1515
private static readonly Regex KeyRegex = new(@"^[\w]+(\[\d+\])?$");
16+
private static readonly Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
1617

1718
private readonly Parser _parser;
1819

@@ -54,8 +55,6 @@ public static NginxConfig LoadFrom(string fileName)
5455
throw new FileNotFoundException(fileName);
5556
}
5657

57-
// var fs = new FileStream(fileName, FileMode.Open, FileAccess.ReadWrite);
58-
5958
var content = File.ReadAllText(fileName);
6059

6160
return Load(content);
@@ -179,7 +178,7 @@ public NginxConfig AddOrUpdate(string keyPath, string value, bool addAsGroup = f
179178

180179
for (int i = 0; i < length; i++)
181180
{
182-
var (key, index) = ResolveKey(paths[i]);
181+
var (key, index, _) = ResolveKey(paths[i]);
183182

184183
IToken find = null;
185184
IEnumerable<IToken> findTokens;
@@ -268,7 +267,7 @@ public NginxConfig Remove(string keyPath)
268267

269268
for (int i = 0; i < length; i++)
270269
{
271-
var (key, index) = ResolveKey(paths[i]);
270+
var (key, index, hasIndex) = ResolveKey(paths[i]);
272271

273272
IToken find = null;
274273
IEnumerable<IToken> findTokens;
@@ -280,19 +279,21 @@ public NginxConfig Remove(string keyPath)
280279

281280
if (i == length - 1)
282281
{
283-
// remove
284-
if (groupToken == null)
282+
IList<IToken> targetList = groupToken == null ? tokens : groupToken.Tokens;
283+
284+
if (hasIndex)
285285
{
286-
foreach (var item in findTokens)
287-
{
288-
tokens.Remove(item);
289-
}
286+
var findTokensCount = findTokens.Count();
287+
if (index < 0 || index >= findTokensCount)
288+
throw new IndexOutOfRangeException($"The key '{key}' index must be >= 0 and < {findTokensCount}");
289+
290+
targetList.Remove(findTokens.ElementAt(index));
290291
}
291292
else
292293
{
293294
foreach (var item in findTokens)
294295
{
295-
groupToken.Tokens.Remove(item);
296+
targetList.Remove(item);
296297
}
297298
}
298299
}
@@ -332,7 +333,7 @@ public void Save(string fileName)
332333
throw new ArgumentException($"'{nameof(fileName)}' cannot be null or whitespace.", nameof(fileName));
333334
}
334335

335-
Save(fileName, Encoding.Default);
336+
Save(fileName, Utf8NoBom);
336337
}
337338

338339
/// <summary>
@@ -378,33 +379,38 @@ public override string ToString()
378379

379380
private void WriteTokenString(IEnumerable<IToken> tokens, TextWriter textWriter, int level = 0)
380381
{
381-
var normalTokens = tokens.Where(x => x is CommentToken || x is ValueToken);
382-
var groupTokens = tokens.Where(x => x is GroupToken);
383-
384382
textWriter.NewLine = Environment.NewLine;
385383

386-
foreach (var token in normalTokens)
384+
var tokenList = tokens as IList<IToken> ?? tokens.ToList();
385+
var wroteAny = false;
386+
387+
foreach (var token in tokenList)
387388
{
388389
if (token is CommentToken comment)
390+
{
389391
textWriter.WriteLine(PadLeftSpace(comment.ToString(), level));
390-
else if (token is ValueToken vaue)
391-
textWriter.WriteLine(PadLeftSpace(vaue.ToString(), level));
392-
}
393-
394-
foreach (GroupToken group in groupTokens)
395-
{
396-
//if (group.Parent != null)
397-
textWriter.WriteLine();
392+
wroteAny = true;
393+
}
394+
else if (token is ValueToken value)
395+
{
396+
textWriter.WriteLine(PadLeftSpace(value.ToString(), level));
397+
wroteAny = true;
398+
}
399+
else if (token is GroupToken group)
400+
{
401+
if (wroteAny)
402+
textWriter.WriteLine();
398403

399-
if (!string.IsNullOrWhiteSpace(group.Comment))
400-
textWriter.WriteLine(PadLeftSpace($"{group.Key} {group.Value} {{ # {group.Comment}", level));
401-
else
402-
textWriter.WriteLine(PadLeftSpace($"{group.Key} {group.Value} {{ ", level));
404+
if (!string.IsNullOrWhiteSpace(group.Comment))
405+
textWriter.WriteLine(PadLeftSpace($"{group.Key} {group.Value} {{ # {group.Comment}", level));
406+
else
407+
textWriter.WriteLine(PadLeftSpace($"{group.Key} {group.Value} {{ ", level));
403408

404-
WriteTokenString(group.Tokens, textWriter, level + 1);
409+
WriteTokenString(group.Tokens, textWriter, level + 1);
405410

406-
// end
407-
textWriter.WriteLine(PadLeftSpace("}", level));
411+
textWriter.WriteLine(PadLeftSpace("}", level));
412+
wroteAny = true;
413+
}
408414
}
409415
}
410416

@@ -423,7 +429,7 @@ private IValueToken GetTokenFromPath(string keyPath)
423429

424430
foreach (var key in paths)
425431
{
426-
var (keyName, index) = ResolveKey(key);
432+
var (keyName, index, _) = ResolveKey(key);
427433

428434
result = FindToken(tokens, keyName, index);
429435
if (result != null)
@@ -446,13 +452,13 @@ private IList<IValueToken> GetTokensFromPath(string keyPath)
446452

447453
var paths = keyPath.Split(':');
448454

449-
IEnumerable<IValueToken> result = null;
455+
IEnumerable<IValueToken> result = Array.Empty<IValueToken>();
450456

451457
IValueToken current = null;
452458

453459
for (int i = 0; i < paths.Length; i++)
454460
{
455-
var (keyName, index) = ResolveKey(paths[i]);
461+
var (keyName, index, _) = ResolveKey(paths[i]);
456462

457463
if (i == paths.Length - 1)
458464
{
@@ -466,6 +472,10 @@ private IList<IValueToken> GetTokensFromPath(string keyPath)
466472
{
467473
tokens = groupToken.Tokens.ToList();
468474
}
475+
else
476+
{
477+
return new List<IValueToken>();
478+
}
469479
}
470480
}
471481

@@ -482,7 +492,7 @@ private static IEnumerable<IValueToken> FindTokens(IEnumerable<IToken> tokens, s
482492
return tokens.Where(x => x is IValueToken valueToken && valueToken.Key == key).Cast<IValueToken>().ToArray();
483493
}
484494

485-
private (string key, int index) ResolveKey(string key)
495+
private (string key, int index, bool hasIndex) ResolveKey(string key)
486496
{
487497
if (!KeyRegex.IsMatch(key))
488498
{
@@ -493,19 +503,21 @@ private static IEnumerable<IValueToken> FindTokens(IEnumerable<IToken> tokens, s
493503

494504
var index = 0;
495505
string keyName = key;
506+
var hasIndex = false;
496507

497508
if (numberStartSymbol > 0)
498509
{
510+
hasIndex = true;
499511
var numberStartIndex = numberStartSymbol + 1;
500512

501513
if (!int.TryParse(key.Substring(numberStartIndex, key.Length - 1 - numberStartIndex), out index))
502514
{
503-
// TODO
515+
throw new Exception($"The key '{key}' index format is incorrect");
504516
}
505517

506518
keyName = key.Substring(0, numberStartSymbol);
507519
}
508520

509-
return (keyName, index);
521+
return (keyName, index, hasIndex);
510522
}
511523
}

NginxConfigParser/NginxConfigParser.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
4+
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0;net10.0</TargetFrameworks>
55
<Authors>Passingwind</Authors>
66
<Version>0.1.4</Version>
77
<PackageLicenseExpression>MIT</PackageLicenseExpression>

NginxConfigParser/Parser.cs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public void Parse()
2424
var lineIndex = 0;
2525

2626
StringReader sr = new StringReader(_content);
27-
string line = string.Empty;
27+
string line;
2828

2929
while ((line = sr.ReadLine()) != null)
3030
{
@@ -50,9 +50,8 @@ private void ParseLine(string text, int lineIndex)
5050
{
5151
var keyEndSymbol = text.IndexOf(' ');
5252
var endSymbol = text.IndexOf(';');
53-
var commendSymbol = text.IndexOf('#');
53+
var commentSymbol = text.IndexOf('#');
5454
var groupStartSymbol = text.IndexOf('{');
55-
var groupEndSymbol = text.IndexOf('}');
5655

5756
if (text.Length == 0)
5857
{
@@ -61,8 +60,8 @@ private void ParseLine(string text, int lineIndex)
6160

6261
if (text[0] == '#')
6362
{
64-
var commendToken = new CommentToken(text.Trim().TrimStart('#').Trim());
65-
AddToken(commendToken);
63+
var commentToken = new CommentToken(text.Trim().TrimStart('#').Trim());
64+
AddToken(commentToken);
6665
return;
6766
}
6867
else if (text[0] == '\'')
@@ -80,7 +79,7 @@ private void ParseLine(string text, int lineIndex)
8079
return;
8180
}
8281

83-
throw new Exception("");
82+
throw new Exception($"Unexpected quoted continuation at line {lineIndex}: {text}");
8483
}
8584
else if (text[0] == '}')
8685
{
@@ -93,7 +92,7 @@ private void ParseLine(string text, int lineIndex)
9392

9493
string key = string.Empty;
9594
string value = string.Empty;
96-
string commend = string.Empty;
95+
string comment = string.Empty;
9796

9897
if (keyEndSymbol > -1)
9998
key = text.Substring(0, keyEndSymbol);
@@ -111,29 +110,29 @@ private void ParseLine(string text, int lineIndex)
111110
value = text.Substring(keyEndSymbol + 1);
112111
}
113112

114-
if (commendSymbol > keyEndSymbol)
115-
commend = text.Substring(commendSymbol + 1).Trim().TrimStart('#').Trim();
113+
if (commentSymbol > keyEndSymbol)
114+
comment = text.Substring(commentSymbol + 1).Trim().TrimStart('#').Trim();
116115

117116
if (groupStartSymbol > -1)
118117
{
119-
var groupToken = new GroupToken(_currentGroupToken, key, value, commend);
118+
var groupToken = new GroupToken(_currentGroupToken, key, value, comment);
120119
AddToken(groupToken);
121120
_currentGroupToken = groupToken;
122121
return;
123122
}
124123

125124
if (groupStartSymbol == -1 && endSymbol == -1)
126125
{
127-
_currentToken = new ValueToken(_currentGroupToken, key, value, commend);
126+
_currentToken = new ValueToken(_currentGroupToken, key, value, comment);
128127
return;
129128
}
130129

131130
if (endSymbol > -1)
132131
{
133-
AddToken(new ValueToken(_currentGroupToken, key, value?.Trim(), commend));
132+
AddToken(new ValueToken(_currentGroupToken, key, value?.Trim(), comment));
134133
return;
135134
}
136135

137-
throw new Exception("");
136+
throw new Exception($"Unable to parse line {lineIndex}: {text}");
138137
}
139138
}

NginxConfigParser/Token.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,3 @@ public override string ToString()
7676
return $"{Key} {Value} => [{Tokens.Count}]";
7777
}
7878
}
79-
80-
public class RootToken
81-
{
82-
}

NginxConfigParser/TokenType.cs

Lines changed: 0 additions & 8 deletions
This file was deleted.

NginxConfigParserTests/NginxConfigParserTests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<IsPackable>false</IsPackable>
77
</PropertyGroup>
88

0 commit comments

Comments
 (0)