feat: add NATS config file lexer (port of Go conf/lex.go)
Port the NATS configuration file lexer from Go's conf/lex.go to C#.
The lexer is a state-machine tokenizer that supports the NATS config
format: key-value pairs with =, :, or whitespace separators; nested
maps {}; arrays []; single and double quoted strings with escape
sequences; block strings (); variables $VAR; include directives;
comments (# and //); booleans; integers with size suffixes (kb, mb, gb);
floats; ISO8601 datetimes; and IP addresses.
This commit is contained in:
1492
src/NATS.Server/Configuration/NatsConfLexer.cs
Normal file
1492
src/NATS.Server/Configuration/NatsConfLexer.cs
Normal file
File diff suppressed because it is too large
Load Diff
24
src/NATS.Server/Configuration/NatsConfToken.cs
Normal file
24
src/NATS.Server/Configuration/NatsConfToken.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
// Port of Go conf/lex.go token types.
|
||||
|
||||
namespace NATS.Server.Configuration;
|
||||
|
||||
public enum TokenType
|
||||
{
|
||||
Error,
|
||||
Eof,
|
||||
Key,
|
||||
String,
|
||||
Bool,
|
||||
Integer,
|
||||
Float,
|
||||
DateTime,
|
||||
ArrayStart,
|
||||
ArrayEnd,
|
||||
MapStart,
|
||||
MapEnd,
|
||||
Variable,
|
||||
Include,
|
||||
Comment,
|
||||
}
|
||||
|
||||
public readonly record struct Token(TokenType Type, string Value, int Line, int Position);
|
||||
221
tests/NATS.Server.Tests/NatsConfLexerTests.cs
Normal file
221
tests/NATS.Server.Tests/NatsConfLexerTests.cs
Normal file
@@ -0,0 +1,221 @@
|
||||
using NATS.Server.Configuration;
|
||||
|
||||
namespace NATS.Server.Tests;
|
||||
|
||||
public class NatsConfLexerTests
|
||||
{
|
||||
[Fact]
|
||||
public void Lex_SimpleKeyStringValue_ReturnsKeyAndString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = \"bar\"").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[0].Value.ShouldBe("foo");
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("bar");
|
||||
tokens[2].Type.ShouldBe(TokenType.Eof);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_SingleQuotedString_ReturnsString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = 'bar'").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("bar");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_IntegerValue_ReturnsInteger()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("port = 4222").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[0].Value.ShouldBe("port");
|
||||
tokens[1].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[1].Value.ShouldBe("4222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_IntegerWithSuffix_ReturnsInteger()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("size = 64mb").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[1].Value.ShouldBe("64mb");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_BooleanValues_ReturnsBool()
|
||||
{
|
||||
foreach (var val in new[] { "true", "false", "yes", "no", "on", "off" })
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize($"flag = {val}").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Bool);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_FloatValue_ReturnsFloat()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("rate = 2.5").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Float);
|
||||
tokens[1].Value.ShouldBe("2.5");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_NegativeNumber_ReturnsInteger()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("offset = -10").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[1].Value.ShouldBe("-10");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_DatetimeValue_ReturnsDatetime()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("ts = 2024-01-15T10:30:00Z").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.DateTime);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_HashComment_IsIgnored()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("# this is a comment\nfoo = 1").ToList();
|
||||
var keys = tokens.Where(t => t.Type == TokenType.Key).ToList();
|
||||
keys.Count.ShouldBe(1);
|
||||
keys[0].Value.ShouldBe("foo");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_SlashComment_IsIgnored()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("// comment\nfoo = 1").ToList();
|
||||
var keys = tokens.Where(t => t.Type == TokenType.Key).ToList();
|
||||
keys.Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_MapBlock_ReturnsMapStartEnd()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("auth { user: admin }").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[0].Value.ShouldBe("auth");
|
||||
tokens[1].Type.ShouldBe(TokenType.MapStart);
|
||||
tokens[2].Type.ShouldBe(TokenType.Key);
|
||||
tokens[2].Value.ShouldBe("user");
|
||||
tokens[3].Type.ShouldBe(TokenType.String);
|
||||
tokens[3].Value.ShouldBe("admin");
|
||||
tokens[4].Type.ShouldBe(TokenType.MapEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_Array_ReturnsArrayStartEnd()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("items = [1, 2, 3]").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.ArrayStart);
|
||||
tokens[2].Type.ShouldBe(TokenType.Integer);
|
||||
tokens[2].Value.ShouldBe("1");
|
||||
tokens[5].Type.ShouldBe(TokenType.ArrayEnd);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_Variable_ReturnsVariable()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("secret = $MY_VAR").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.Variable);
|
||||
tokens[1].Value.ShouldBe("MY_VAR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_Include_ReturnsInclude()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("include \"auth.conf\"").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Include);
|
||||
tokens[0].Value.ShouldBe("auth.conf");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_EscapeSequences_AreProcessed()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("msg = \"hello\\tworld\\n\"").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("hello\tworld\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_HexEscape_IsProcessed()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("val = \"\\x41\\x42\"").ToList();
|
||||
tokens[1].Value.ShouldBe("AB");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_ColonSeparator_Works()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo: bar").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_WhitespaceSeparator_Works()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo bar").ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_SemicolonTerminator_IsHandled()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = 1; bar = 2").ToList();
|
||||
var keys = tokens.Where(t => t.Type == TokenType.Key).ToList();
|
||||
keys.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_EmptyInput_ReturnsEof()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("").ToList();
|
||||
tokens.Count.ShouldBe(1);
|
||||
tokens[0].Type.ShouldBe(TokenType.Eof);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_BlockString_ReturnsString()
|
||||
{
|
||||
var input = "desc (\nthis is\na block\n)\n";
|
||||
var tokens = NatsConfLexer.Tokenize(input).ToList();
|
||||
tokens[0].Type.ShouldBe(TokenType.Key);
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_IPAddress_ReturnsString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("host = 127.0.0.1").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("127.0.0.1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_TrackLineNumbers()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("a = 1\nb = 2\nc = 3").ToList();
|
||||
tokens[0].Line.ShouldBe(1); // a
|
||||
tokens[2].Line.ShouldBe(2); // b
|
||||
tokens[4].Line.ShouldBe(3); // c
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_UnterminatedString_ReturnsError()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = \"unterminated").ToList();
|
||||
tokens.ShouldContain(t => t.Type == TokenType.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lex_StringStartingWithDigit_TreatedAsString()
|
||||
{
|
||||
var tokens = NatsConfLexer.Tokenize("foo = 3xyz").ToList();
|
||||
tokens[1].Type.ShouldBe(TokenType.String);
|
||||
tokens[1].Value.ShouldBe("3xyz");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user