Skip to content

Commit e5b879f

Browse files
mitikovmr.slow
andauthored
Enrich expression API (#71)
* Doc ends with period * Inherit code doc over copy-pasting * Introduce cluster props concept, hidden though * Add handy API to check cluster info * Add GetProperty API to simplify API usages * Let evaluate either expression based on condition * Ease debugging by allowing Expressions to print inner content for some * Add toNumber converting method * Increase safety harness for newly added code * Let test failures be self-speakable * Fix failing tests thanks to fresh intel Co-authored-by: mr.slow <no-reply@github.com>
1 parent 8393100 commit e5b879f

3 files changed

Lines changed: 155 additions & 21 deletions

File tree

src/AzureMapsControl.Components/Atlas/Expression.cs

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
{
33
using System;
44
using System.Collections.Generic;
5+
using System.Diagnostics;
56
using System.Diagnostics.CodeAnalysis;
6-
using System.Linq;
77
using System.Text.Json;
88
using System.Text.Json.Serialization;
99

@@ -24,41 +24,102 @@ public class Expression
2424
internal Expression() { }
2525

2626
/// <summary>
27-
/// Creates an expression
27+
/// Creates an expression.
2828
/// </summary>
29-
/// <param name="expressions">Expressions to include in this expression</param>
29+
/// <param name="expressions">Expressions to include in this expression.</param>
3030
public Expression(IEnumerable<Expression> expressions) => Expressions = expressions;
3131

3232
/// <summary>
33-
/// Creates an expression
33+
/// Creates an expression.
3434
/// </summary>
35-
/// <param name="json">Json representation of the expression</param>
35+
/// <param name="json">Json representation of the expression.</param>
3636
public Expression(JsonDocument json) => Json = json;
37+
38+
/// <summary>
39+
/// Converts <see cref="this"/> resulting value to number.
40+
/// <para>Wrapper around 'to-number': <see cref="https://docs.microsoft.com/en-us/azure/azure-maps/data-driven-style-expressions-web-sdk"/>.</para>
41+
/// </summary>
42+
/// <returns>An expression converting supplied expression result into number.</returns>
43+
public ExpressionOrNumber ToNumber()
44+
=> this is ExpressionOrNumber alreadyNumber
45+
? alreadyNumber
46+
: (new(new[] { new ExpressionOrString("to-number"), this }));
47+
48+
private static readonly Expression s_getter = new ExpressionOrString("get");
49+
50+
/// <summary>
51+
/// An expression getting property value by <paramref name="propertyName"/>.
52+
/// <para>Cluster properties are supplied via <seealso cref="Data.DataSourceOptions.ClusterProperties"/>.</para>
53+
/// <para>Leaf level properties are supplied in data itself (f.e. <see cref="Feature.Properties"/>).</para>
54+
/// </summary>
55+
/// <param name="propertyName">The property name to get value.</param>
56+
/// <returns>An expression to fetch property value.</returns>
57+
public static Expression GetProperty(string propertyName) => new(new[] { s_getter, new ExpressionOrString(propertyName) });
58+
59+
/// <summary>
60+
/// An expression checking if <paramref name="propertyName"/> is defined in node.
61+
/// <para>Typically used during data clustering to check if cluster node has property</para>
62+
/// <para>See <seealso cref="Data.DataSourceOptions.ClusterProperties"/>.</para>
63+
/// </summary>
64+
/// <param name="propertyName">The property name to check existance.</param>
65+
/// <returns>Expression that will evaluate into <c>true</c> if cluster has property; <c>false</c> otherwise.</returns>
66+
public static Expression HasProperty(string propertyName) => new(new[] { new ExpressionOrString("has"), new ExpressionOrString(propertyName) });
67+
68+
/// <summary>
69+
/// An expression conditionally evaluating either <paramref name="ifTrue"/> or <paramref name="ifFalse"/> based on <paramref name="condition"/>.
70+
/// <para>Typically used during cluster/leaf property fetch, as cluster has only aggregated properties, while leaf level has more.</para>
71+
/// </summary>
72+
/// <param name="condition">The expression evaluating to <see cref="bool"/> (f.e. <see cref="IsCluster"/>).</param>
73+
/// <param name="ifTrue">The expression to evaluate if <paramref name="condition"/> was <c>true</c>.</param>
74+
/// <param name="ifFalse">The expression to evaluate if <paramref name="condition"/> was <c>false</c>.</param>
75+
/// <returns>An expression conditionally evaluating either expression based on <paramref name="condition"/>.</returns>
76+
public static Expression Conditional(Expression condition, Expression ifTrue, Expression ifFalse) => new(new[] { new ExpressionOrString("case"), condition, ifTrue, ifFalse });
77+
78+
/// <summary>
79+
/// An expression checking if node is cluster, or leaf.
80+
/// <para>See <seealso cref="Data.DataSourceOptions.Cluster"/></para>
81+
/// </summary>
82+
public static readonly Expression IsCluster = HasProperty(ClusterProperties.PointCount);
83+
84+
/// <summary>
85+
/// Holds cluster-specific properties provided by clustering engine, see <seealso cref="Data.DataSourceOptions.Cluster"/>.
86+
/// <para>
87+
/// <seealso cref="https://docs.microsoft.com/en-us/azure/azure-maps/clustering-point-data-web-sdk"/>
88+
/// </para>
89+
/// </summary>
90+
private struct ClusterProperties
91+
{
92+
/// <summary>
93+
/// Point count exists only for cluster-level; leaf-level nodes do not have it.
94+
/// </summary>
95+
public static readonly string PointCount = "point_count";
96+
}
3797
}
3898

3999
/// <summary>
40100
/// Can be specified as the value of filter or certain layer options.
41101
/// </summary>
42102
[JsonConverter(typeof(ExpressionOrNumberJsonConverter))]
43103
[ExcludeFromCodeCoverage]
104+
[DebuggerDisplay("{" + nameof(Value) + "}")]
44105
public sealed class ExpressionOrNumber : Expression
45106
{
46107
internal double? Value { get; }
47108

48109
/// <summary>
49-
/// Creates an expression
110+
/// <inheritdoc cref="Expression(IEnumerable{Expression})"/>
50111
/// </summary>
51-
/// <param name="expressions">Expressions to include in this expression</param>
112+
/// <param name="expressions"><inheritdoc/></param>
52113
public ExpressionOrNumber(IEnumerable<Expression> expressions) : base(expressions) { }
53114

54115
/// <summary>
55-
/// Creates an expression
116+
/// <inheritdoc cref="Expression(JsonDocument)"/>
56117
/// </summary>
57-
/// <param name="json">Json representation of the expression</param>
118+
/// <param name="json"><inheritdoc/></param>
58119
public ExpressionOrNumber(JsonDocument json) : base(json) { }
59120

60121
/// <summary>
61-
/// Creates an expression
122+
/// Creates an expression.
62123
/// </summary>
63124
/// <param name="value">Value which will be used instead of the expression</param>
64125
public ExpressionOrNumber(double? value) => Value = value;
@@ -69,20 +130,21 @@ public ExpressionOrNumber(JsonDocument json) : base(json) { }
69130
/// </summary>
70131
[JsonConverter(typeof(ExpressionOrStringJsonConverter))]
71132
[ExcludeFromCodeCoverage]
133+
[DebuggerDisplay("{" + nameof(Value) + "}")]
72134
public sealed class ExpressionOrString : Expression
73135
{
74136
internal string Value { get; }
75137

76138
/// <summary>
77-
/// Creates an expression
139+
/// <inheritdoc cref="Expression(IEnumerable{Expression})"/>
78140
/// </summary>
79-
/// <param name="expressions">Expressions to include in this expression</param>
141+
/// <param name="expressions"><inheritdoc/></param>
80142
public ExpressionOrString(IEnumerable<Expression> expressions) : base(expressions) { }
81143

82144
/// <summary>
83-
/// Creates an expression
145+
/// <inheritdoc cref="Expression(JsonDocument)"/>
84146
/// </summary>
85-
/// <param name="json">Json representation of the expression</param>
147+
/// <param name="json"><inheritdoc/></param>
86148
public ExpressionOrString(JsonDocument json) : base(json) { }
87149

88150
/// <summary>
@@ -102,19 +164,19 @@ public sealed class ExpressionOrStringArray : Expression
102164
internal IEnumerable<string> Values { get; }
103165

104166
/// <summary>
105-
/// Creates an expression
167+
/// <inheritdoc cref="Expression(IEnumerable{Expression})"/>
106168
/// </summary>
107-
/// <param name="expressions">Expressions to include in this expression</param>
169+
/// <param name="expressions"><inheritdoc/></param>
108170
public ExpressionOrStringArray(IEnumerable<Expression> expressions) : base(expressions) { }
109171

110172
/// <summary>
111-
/// Creates an expression
173+
/// <inheritdoc cref="Expression(JsonDocument)"/>
112174
/// </summary>
113-
/// <param name="json">Json representation of the expression</param>
175+
/// <param name="json"><inheritdoc/></param>
114176
public ExpressionOrStringArray(JsonDocument json): base(json) { }
115177

116178
/// <summary>
117-
/// Creates an expression
179+
/// Creates an expression.
118180
/// </summary>
119181
/// <param name="values">Values of the expression</param>
120182
public ExpressionOrStringArray(IEnumerable<string> values) => Values = values;

tests/AzureMapsControl.Components.Tests/Atlas/Expression.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
namespace AzureMapsControl.Components.Tests.Atlas
22
{
33
using System;
4+
using System.Diagnostics;
45
using System.Text.Json;
56

67
using AzureMapsControl.Components.Atlas;
@@ -31,6 +32,61 @@ public void Should_WriteExpressions()
3132
var expectedJson = "[[\"get\",\"Confirmed\"]]";
3233
TestAndAssertWrite(expression, expectedJson);
3334
}
35+
36+
[Fact]
37+
public void GetProperty_WhenCalled_ProducesCompliantJson()
38+
{
39+
var propertyName = "Confirmed";
40+
var expression = Expression.GetProperty(propertyName);
41+
TestAndAssertWrite(expression, @$"[""get"",""{propertyName}""]");
42+
}
43+
44+
[Fact]
45+
public void HasProperty_WhenCalled_ProducesCompliantJson()
46+
{
47+
var propertyName = "Confirmed";
48+
var expression = Expression.HasProperty(propertyName);
49+
TestAndAssertWrite(expression, @$"[""has"",""{propertyName}""]");
50+
}
51+
52+
[Fact]
53+
public void IsCluster_WhenCalled_ProducesCompliantJson()
54+
=> TestAndAssertWrite(Expression.IsCluster, expectedJson: @"[""has"",""point_count""]");
55+
56+
[Fact]
57+
public void Conditional_WhenCalled_ProducesCompliantJson()
58+
{
59+
var clusterProp = Expression.GetProperty("clusterValue");
60+
var leafProp = Expression.GetProperty("leafValue");
61+
var expression = Expression.Conditional(Expression.IsCluster, clusterProp, leafProp);
62+
TestAndAssertWrite(expression, @"[""case"",[""has"",""point_count""],[""get"",""clusterValue""],[""get"",""leafValue""]]");
63+
}
64+
65+
[Fact]
66+
public void ToNumber_WhenNotYetNumber_Wraps()
67+
{
68+
var propGetter = Expression.GetProperty("iAmNumber");
69+
var expression = propGetter.ToNumber();
70+
TestAndAssertWrite(expression, @"[""to-number"",[""get"",""iAmNumber""]]");
71+
}
72+
}
73+
74+
public class ExpressionOrNumberTests
75+
{
76+
[Fact]
77+
public void Type_Is_DebugFriendly()
78+
{
79+
var attributes = typeof(ExpressionOrNumber).GetCustomAttributesData();
80+
Assert.Contains(attributes, attribute => attribute.AttributeType == typeof(DebuggerDisplayAttribute));
81+
}
82+
83+
[Fact]
84+
public void ToNumber_WhenAlreadyNumber_ReturnsSelf()
85+
{
86+
Expression alreadyNumber = new ExpressionOrNumber(5);
87+
var expression = alreadyNumber.ToNumber();
88+
Assert.Same(alreadyNumber, expression);
89+
}
3490
}
3591

3692
public class ExpressionOrNumberJsonConverterTests : JsonConverterTests<ExpressionOrNumber>
@@ -76,6 +132,16 @@ public void Should_NotWriteNumberValue()
76132
}
77133
}
78134

135+
public class ExpressionOrStringTests
136+
{
137+
[Fact]
138+
public void Type_Is_DebugFriendly()
139+
{
140+
var attributes = typeof(ExpressionOrString).GetCustomAttributesData();
141+
Assert.Contains(attributes, attribute => attribute.AttributeType == typeof(DebuggerDisplayAttribute));
142+
}
143+
}
144+
79145
public class ExpressionOrStringJsonConverterTests : JsonConverterTests<ExpressionOrString>
80146
{
81147
public ExpressionOrStringJsonConverterTests() : base(new ExpressionOrStringJsonConverter()) { }

tests/AzureMapsControl.Components.Tests/Json/JsonConverterTests.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
namespace AzureMapsControl.Components.Tests.Json
22
{
3+
using System;
34
using System.Buffers;
5+
using System.IO;
46
using System.Text;
57
using System.Text.Json;
68
using System.Text.Json.Serialization;
@@ -22,12 +24,16 @@ protected void TestAndAssertWrite(TValue value, string expectedJson)
2224

2325
writer.Flush();
2426

27+
var serializedBytes = buffer.WrittenSpan.ToArray();
28+
29+
var restored = Encoding.UTF8.GetString(serializedBytes);
2530
var expectedBytes = Encoding.UTF8.GetBytes(expectedJson);
2631

2732
var expectedBytesSet = new System.Collections.Generic.HashSet<byte>(expectedBytes);
28-
var writterSet = new System.Collections.Generic.HashSet<byte>(buffer.WrittenSpan.ToArray());
33+
var writterSet = new System.Collections.Generic.HashSet<byte>(serializedBytes);
2934

30-
Assert.Equal(expectedBytes.Length, buffer.WrittenCount);
35+
var haveSameLength = expectedBytes.Length == buffer.WrittenCount;
36+
Assert.True(haveSameLength, userMessage: $"Different length detected, expected:{Environment.NewLine}'{expectedJson}'{Environment.NewLine}Actual:{Environment.NewLine}'{restored}'");
3137
Assert.Subset(expectedBytesSet, writterSet);
3238
}
3339

0 commit comments

Comments
 (0)