diff --git a/src/PromQL.Parser/Printer.cs b/src/PromQL.Parser/Printer.cs
index d172c3c..f3f6fe6 100644
--- a/src/PromQL.Parser/Printer.cs
+++ b/src/PromQL.Parser/Printer.cs
@@ -1,21 +1,38 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text;
using PromQL.Parser.Ast;
namespace PromQL.Parser
{
- // TODO fix bug around "and"
- // TODO pretty print support
+ ///
+ /// Converts PromQL Abstract Syntax Tress (AST's) into a string representation.
+ ///
+ ///
+ /// Instances of this class are not thread safe and so should not be shared between threads.
+ ///
public class Printer : IVisitor
{
- private StringBuilder _sb = new ();
+ private readonly PrinterOptions _options;
+ private readonly IndentedStringBuilder _sb;
+
+ ///
+ /// Initializes a new printer instance.
+ ///
+ /// Allows customization of the output generated by the printer. Defaults to .
+ public Printer(PrinterOptions? options = null)
+ {
+ _options = options ?? PrinterOptions.PrettyDefault;
+ _sb = new IndentedStringBuilder(_options.IndentChar, _options.IndentCount);
+ }
public virtual void Visit(StringLiteral s)
{
// Raw strings are denoted by ` and should not have their values escaped
if (s.Quote == '`')
{
- _sb.Append($"{s.Quote}{s.Value}{s.Quote}");
+ Write($"{s.Quote}{s.Value}{s.Quote}");
return;
}
@@ -31,61 +48,61 @@ public virtual void Visit(StringLiteral s)
.Replace("\t", "\\t")
.Replace("\v", "\\v");
- _sb.Append($"{s.Quote}{escaped}{s.Quote}");
+ Write($"{s.Quote}{escaped}{s.Quote}");
}
public virtual void Visit(SubqueryExpr sq)
{
sq.Expr.Accept(this);
- _sb.Append("[");
+ Write("[");
sq.Range.Accept(this);
- _sb.Append(":");
+ Write(":");
sq.Step?.Accept(this);
- _sb.Append("]");
+ Write("]");
}
public virtual void Visit(Duration d)
{
if (d.Value.Days > 0)
- _sb.Append($"{d.Value.Days}d");
+ Write($"{d.Value.Days}d");
if (d.Value.Hours > 0)
- _sb.Append($"{d.Value.Hours}h");
+ Write($"{d.Value.Hours}h");
if (d.Value.Minutes > 0)
- _sb.Append($"{d.Value.Minutes}m");
+ Write($"{d.Value.Minutes}m");
if (d.Value.Seconds > 0)
- _sb.Append($"{d.Value.Seconds}s");
+ Write($"{d.Value.Seconds}s");
if (d.Value.Milliseconds > 0)
- _sb.Append($"{d.Value.Milliseconds}ms");
+ Write($"{d.Value.Milliseconds}ms");
}
- public virtual void Visit(NumberLiteral n) => _sb.Append(n.Value switch
+ public virtual void Visit(NumberLiteral n) => Write(n.Value switch
{
double.PositiveInfinity => "Inf",
double.NegativeInfinity => "-Inf",
_ => n.Value.ToString()
});
- public virtual void Visit(MetricIdentifier mi) => _sb.Append(mi.Value);
+ public virtual void Visit(MetricIdentifier mi) => Write(mi.Value);
public virtual void Visit(LabelMatcher lm)
{
- _sb.Append($"{lm.LabelName}{lm.Operator.ToPromQl()}");
+ Write($"{lm.LabelName}{lm.Operator.ToPromQl()}");
lm.Value.Accept(this);
}
public virtual void Visit(LabelMatchers lms)
{
bool first = true;
- _sb.Append("{");
+ Write("{");
foreach (var lm in lms.Matchers)
{
if (!first)
- _sb.Append(", ");
+ Write(", ");
lm.Accept(this);
first = false;
}
- _sb.Append("}");
+ Write("}");
}
public virtual void Visit(VectorSelector vs)
@@ -96,22 +113,22 @@ public virtual void Visit(VectorSelector vs)
public virtual void Visit(UnaryExpr unary)
{
- _sb.Append(unary.Operator.ToPromQl());
+ Write(unary.Operator.ToPromQl());
unary.Expr.Accept(this);
}
public virtual void Visit(MatrixSelector ms)
{
ms.Vector.Accept(this);
- _sb.Append("[");
+ Write("[");
ms.Duration.Accept(this);
- _sb.Append("]");
+ Write("]");
}
public virtual void Visit(OffsetExpr offset)
{
offset.Expr.Accept(this);
- _sb.Append(" offset ");
+ Write(" offset ");
Duration d = offset.Duration;
@@ -119,7 +136,7 @@ public virtual void Visit(OffsetExpr offset)
{
// Negative durations cannot be printed by the duration visitor. Convert to positive and emit sign here.
d = d with { Value = new TimeSpan(Math.Abs(d.Value.Ticks))};
- _sb.Append("-");
+ Write("-");
}
d.Accept(this);
@@ -127,60 +144,65 @@ public virtual void Visit(OffsetExpr offset)
public virtual void Visit(ParenExpression paren)
{
- _sb.Append("(");
- paren.Expr.Accept(this);
- _sb.Append(")");
+ Write("(");
+
+ using (_options.BreakOnParenExpr ? _sb.IncreaseIndent() : null)
+ paren.Expr.Accept(this);
+
+ Write(")");
}
public virtual void Visit(FunctionCall fnCall)
{
- _sb.Append($"{fnCall.Function.Name}(");
+ Write($"{fnCall.Function.Name}(");
bool isFirst = true;
foreach (var arg in fnCall.Args)
{
if (!isFirst)
- _sb.Append(", ");
+ Write(", ");
arg.Accept(this);
isFirst = false;
}
- _sb.Append(")");
+ Write(")");
}
public virtual void Visit(VectorMatching vm)
{
if (vm.ReturnBool)
- _sb.Append("bool");
+ {
+ Write("bool");
+ }
if (vm.On || vm.MatchingLabels.Length > 0)
{
- if (_sb.Length > 0)
- _sb.Append(" ");
+ if (vm.ReturnBool)
+ Write(" ");
- _sb.Append(vm.On ? "on" : "ignoring");
- _sb.Append(" (");
- _sb.Append(string.Join(", ", vm.MatchingLabels));
- _sb.Append(")");
+ Write(vm.On ? "on" : "ignoring");
+ Write(" (");
+ Write(string.Join(", ", vm.MatchingLabels));
+ Write(")");
}
if (vm.MatchCardinality != VectorMatching.DefaultMatchCardinality)
{
if (_sb.Length > 0)
- _sb.Append(" ");
+ Write(" ");
- _sb.Append(vm.MatchCardinality.ToPromQl());
+ Write(vm.MatchCardinality.ToPromQl()!);
}
if (vm.Include.Length > 0 || vm.MatchCardinality != VectorMatching.DefaultMatchCardinality)
{
if (_sb.Length > 0)
- _sb.Append(" ");
+ Write(" ");
- _sb.Append("(");
- _sb.Append(string.Join(", ", vm.Include));
- _sb.Append(")");
+ Write("(");
+ Write(string.Join(", ", vm.Include));
+ Write(")");
}
}
@@ -188,40 +210,48 @@ public virtual void Visit(BinaryExpr expr)
{
expr.LeftHandSide.Accept(this);
- _sb.Append($" {expr.Operator.ToPromQl()} ");
+ if (_options.BreakOnBinaryOperators)
+ _sb.AppendLine();
+ else
+ Write(" ");
+
+ Write($"{expr.Operator.ToPromQl()} ");
var preLen = _sb.Length;
expr.VectorMatching?.Accept(this);
if (_sb.Length > preLen)
- _sb.Append(" ");
+ Write(" ");
expr.RightHandSide.Accept(this);
}
public virtual void Visit(AggregateExpr expr)
{
- _sb.Append($"{expr.Operator.Name}");
+ Write($"{expr.Operator.Name}");
if (expr.GroupingLabels.Length > 0)
{
- _sb.Append(" ");
- _sb.Append(expr.Without ? "without" : "by");
- _sb.Append($" ({string.Join(", ", expr.GroupingLabels)}) ");
+ Write(" ");
+ Write(expr.Without ? "without" : "by");
+ Write($" ({string.Join(", ", expr.GroupingLabels)}) ");
}
- _sb.Append("(");
+ Write("(");
if (expr.Param != null)
{
expr.Param.Accept(this);
- _sb.Append(", ");
+ Write(", ");
}
expr.Expr.Accept(this);
- _sb.Append(")");
+ Write(")");
}
- protected void Write(string s) => _sb.Append(s);
+ protected void Write(string s)
+ {
+ _sb.Append(s);
+ }
public string ToPromQl(IPromQlNode node)
{
@@ -231,7 +261,92 @@ public string ToPromQl(IPromQlNode node)
_sb.Clear();
node.Accept(this);
- return _sb.ToString();
+ return _sb.ToString()!;
+ }
+ }
+
+ internal class IndentedStringBuilder
+ {
+ private int _indentLevel = 0;
+ private readonly string[] _indents;
+ private const int MaxIndent = 20;
+ private StringBuilder _sb = new StringBuilder();
+
+ public IndentedStringBuilder(char indentChar, int indentCount)
+ {
+ _indents = Enumerable.Range(0, MaxIndent)
+ .Select(n => new string(Enumerable.Repeat(indentChar, indentCount * n).ToArray()))
+ .ToArray();
+ }
+
+ public void Append(string? s) => _sb.Append(s);
+
+ public int Length => _sb.Length;
+
+ public void Clear() => _sb.Clear();
+
+ public void AppendLine()
+ {
+ _sb.AppendLine();
+ _sb.Append(_indents[_indentLevel]);
}
+
+ public override string ToString() => _sb.ToString();
+
+ public IDisposable IncreaseIndent()
+ {
+ _indentLevel++;
+ if (_indentLevel == MaxIndent)
+ throw new InvalidOperationException($"Cannot increase indent beyond {MaxIndent}");
+
+ AppendLine();
+ return new DisposableIndent(this);
+ }
+
+ public void DecreaseIndent()
+ {
+ _indentLevel--;
+ AppendLine();
+ }
+
+ internal class DisposableIndent : IDisposable
+ {
+ private readonly IndentedStringBuilder _isb;
+ private bool _disposed;
+
+ public DisposableIndent(IndentedStringBuilder isb)
+ {
+ _isb = isb;
+ _disposed = false;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _isb.DecreaseIndent();
+ _disposed = true;
+ }
+ }
+ }
+
+ public record PrinterOptions(
+ int IndentCount,
+ char IndentChar,
+ bool BreakOnParenExpr,
+ bool BreakOnBinaryOperators
+ )
+ {
+ ///
+ /// Formats the printed output of PromQL expressions into a more human-readable format,
+ /// by inserting line breaks and indentation.
+ ///
+ public static PrinterOptions PrettyDefault = new(2, ' ', true, true);
+
+ ///
+ /// Doesn't format the printed output of PromQL expressions.
+ ///
+ public static PrinterOptions NoFormatting = new(0, ' ', false, false);
}
}
\ No newline at end of file
diff --git a/tests/PromQL.Parser.Tests/ExtensibilityTests.cs b/tests/PromQL.Parser.Tests/ExtensibilityTests.cs
index 0f42276..9ea0f6c 100644
--- a/tests/PromQL.Parser.Tests/ExtensibilityTests.cs
+++ b/tests/PromQL.Parser.Tests/ExtensibilityTests.cs
@@ -53,6 +53,10 @@ from num in Span.EqualTo("$__rate_interval").Try().Or(Span.EqualTo("$__interval"
public class CustomPrinter : Printer
{
+ public CustomPrinter() : base(PrinterOptions.NoFormatting)
+ {
+ }
+
public override void Visit(Duration d)
{
if (d.Value == new TimeSpan(-1))
diff --git a/tests/PromQL.Parser.Tests/PrinterTests.cs b/tests/PromQL.Parser.Tests/PrinterTests.cs
index 72d932a..40e5e30 100644
--- a/tests/PromQL.Parser.Tests/PrinterTests.cs
+++ b/tests/PromQL.Parser.Tests/PrinterTests.cs
@@ -7,9 +7,9 @@
namespace PromQL.Parser.Tests
{
[TestFixture]
- public class PrettyPrinterTests
+ public class PrinterTests
{
- private Printer _printer = new Printer();
+ private Printer _printer = new Printer(PrinterOptions.NoFormatting);
[Test]
public void StringLiteral_SingleQuote() => _printer.ToPromQl(new StringLiteral('\'', "hello")).Should().Be("'hello'");
@@ -158,6 +158,23 @@ public void Offset_Negative()
new Duration(TimeSpan.FromHours(-5))))
.Should().Be("foo offset -5h");
}
+
+ [Test]
+ public void BinaryExpr_OnNoLabels_ToPromQl()
+ {
+ _printer.ToPromQl(new BinaryExpr(
+ new NumberLiteral(1.0),
+ new NumberLiteral(2.0),
+ Operators.Binary.Add,
+ new VectorMatching(
+ Operators.VectorMatchCardinality.OneToOne,
+ ImmutableArray.Empty,
+ true,
+ ImmutableArray.Empty,
+ false
+ )
+ )).Should().Be("1 + on () 2");
+ }
// This expression doesn't have to be valid PromQL to be a useful test
[Test]
@@ -190,4 +207,37 @@ public void Complex_ToPromQl() =>
null
)).Should().Be("(another_metric{one='test', two!='test2'}[1h][1d:5m]) + -vector(this_is_a_metric offset 5m)");
}
+
+ [TestFixture]
+ public class PrettyPrintTests
+ {
+ private Printer _printer = new Printer(PrinterOptions.PrettyDefault);
+
+ [Test]
+ public void ParenExpression() => _printer.ToPromQl(new ParenExpression(new NumberLiteral(1.0)))
+ .Should().Be(@"(
+ 1
+)");
+
+ [Test]
+ public void ParenExpressionNested() => _printer.ToPromQl(new ParenExpression(new ParenExpression(new NumberLiteral(1.0))))
+ .Should().Be(@"(
+ (
+ 1
+ )
+)");
+
+ [Test]
+ public void BinaryExpression() => _printer.ToPromQl(new BinaryExpr(new NumberLiteral(1.0), new NumberLiteral(1.0), Operators.Binary.Add))
+ .Should().Be(@"1
++ 1");
+
+ [Test]
+ public void ParenBinaryExpression() => _printer.ToPromQl(new ParenExpression(new BinaryExpr(new NumberLiteral(1.0), new NumberLiteral(1.0), Operators.Binary.Add)))
+ .Should().Be(@"(
+ 1
+ + 1
+)");
+
+ }
}
\ No newline at end of file