• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

LBreedlove / Queuebal.Expressions / 17180697007

23 Aug 2025 10:10PM UTC coverage: 95.681% (-0.6%) from 96.288%
17180697007

push

github

web-flow
Add JoinExpression and .editorconfig (#16)

408 of 414 branches covered (98.55%)

Branch coverage included in aggregate %.

56 of 77 new or added lines in 1 file covered. (72.73%)

2516 of 2642 relevant lines covered (95.23%)

454.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.34
/Queuebal.Expressions/JoinExpression.cs
1
using Queuebal.Json;
2

3
namespace Queuebal.Expressions;
4

5

6
public class JoinField
7
{
8
    /// <summary>
9
    /// The name of the source field to include in the joined object.
10
    /// </summary>
11
    /// <remarks>
12
    /// This field may be '*' to indicate that all fields from the source object should be included.
13
    /// If '*' is used, the Alias property will be ignored.
14
    /// </remarks>
15
    public required string FieldName { get; set; }
8✔
16

17
    /// <summary>
18
    /// The name to use when writing the field to the joined object.
19
    /// If not specified, the original field name will be used.
20
    /// </summary>
21
    public string? Alias { get; set; }
5✔
22
}
23

24

25
/// <summary>
26
/// An expression that joins objects from two lists and creates a single list of objects, with
27
/// values from both lists.
28
/// NOTE: The performance of this expression is not optimal, as it uses a nested loop to join the objects.
29
/// It is recommended to use this expression only for small lists.
30
/// </summary>
31
public class JoinExpression : Expression
32
{
33
    /// <summary>
34
    /// Gets the name of the expression type.
35
    /// This property MUST be overridden in derived classes to specify the type of expression.
36
    /// </summary>
37
    public static string ExpressionType { get; } = "Join";
8✔
38

39
    /// <summary>
40
    /// An expression that evaluates to the left list of objects to join.
41
    /// </summary>
42
    public required IExpression LTable { get; set; }
2✔
43

44
    /// <summary>
45
    /// An expression that evaluates to the right list of objects to join.
46
    /// </summary>
47
    public required IExpression RTable { get; set; }
2✔
48

49
    /// <summary>
50
    /// The key selector expression used to extract the key from the left objects for joining.
51
    /// </summary>
52
    /// <remarks>
53
    /// If the key selector is a RawValue with a string value, it will be treated as a field name to extract from the left objects.
54
    /// Otherwise, the input to the key selector will be a dict with the shape `{ "key": fieldName, "value": fieldValue }`.
55
    /// </remarks>
56
    public required IExpression LeftKeySelector { get; set; }
3✔
57

58
    /// <summary>
59
    /// The key selector expression used to extract the key from the right objects for joining.
60
    /// </summary>
61
    /// <remarks>
62
    /// If the key selector is a RawValue with a string value, it will be treated as a field name to extract from the right objects.
63
    /// Otherwise, the input to the key selector will be a dict with the shape `{ "key": fieldName, "value": fieldValue }`.
64
    /// </remarks>
65
    public required IExpression RightKeySelector { get; set; }
5✔
66

67
    /// <summary>
68
    /// The fields to include from the left table in the resulting joined objects.
69
    /// </summary>
70
    public required List<JoinField> LTableFields { get; set; }
2✔
71

72
    /// <summary>
73
    /// The fields to include from the right table in the resulting joined objects.
74
    /// </summary>
75
    public required List<JoinField> RTableFields { get; set; }
2✔
76

77
    protected override JSONValue EvaluateExpression(ExpressionContext context, JSONValue inputValue)
78
    {
1✔
79
        var lvalues = LTable.Evaluate(context, inputValue);
1✔
80
        if (!lvalues.IsList)
1✔
NEW
81
        {
×
NEW
82
            throw new InvalidOperationException("Left table must evaluate to a list.");
×
83
        }
84

85
        var rvalues = RTable.Evaluate(context, inputValue);
1✔
86
        if (!rvalues.IsList)
1✔
NEW
87
        {
×
NEW
88
            throw new InvalidOperationException("Right table must evaluate to a list.");
×
89
        }
90

91
        List<JSONValue> results = new();
1✔
92
        foreach (var lrecord in lvalues.ListValue)
7✔
93
        {
2✔
94
            if (!lrecord.IsDict)
2✔
NEW
95
            {
×
NEW
96
                throw new InvalidOperationException("Left table records must be dictionaries.");
×
97
            }
98

99
            var lkey = LeftKeySelector.Evaluate(context, lrecord);
2✔
100
            if (lkey.IsList || lkey.IsDict)
2✔
NEW
101
            {
×
NEW
102
                throw new InvalidOperationException("Left key selector cannot be a list or dictionary.");
×
103
            }
104

105
            foreach (var rrecord in rvalues.ListValue)
14✔
106
            {
4✔
107
                if (!rrecord.IsDict)
4✔
NEW
108
                {
×
NEW
109
                    throw new InvalidOperationException("Right table records must be dictionaries.");
×
110
                }
111

112
                var rkey = RightKeySelector.Evaluate(context, rrecord);
4✔
113
                if (rkey.IsList || rkey.IsDict)
4✔
NEW
114
                {
×
NEW
115
                    throw new InvalidOperationException("Right key selector cannot be a list or dictionary.");
×
116
                }
117

118
                if (!lkey.Equals(rkey))
4✔
119
                {
3✔
120
                    continue;
3✔
121
                }
122

123
                results.Add(CreateRecord(lrecord, rrecord));
1✔
124
            }
1✔
125
        }
2✔
126

127
        return results;
1✔
128
    }
1✔
129

130
    private JSONValue CreateRecord(JSONValue lrecord, JSONValue rrecord)
131
    {
1✔
132
        var result = new Dictionary<string, JSONValue>();
1✔
133
        foreach (var kvp in GetFields(lrecord, LTableFields))
7✔
134
        {
2✔
135
            result[kvp.Key] = kvp.Value;
2✔
136
        }
2✔
137

138
        foreach (var kvp in GetFields(rrecord, RTableFields))
5✔
139
        {
1✔
140
            result[kvp.Key] = kvp.Value;
1✔
141
        }
1✔
142

143
        return result;
1✔
144
    }
1✔
145

146
    private IEnumerable<KeyValuePair<string, JSONValue>> GetFields(JSONValue record, List<JoinField> fields)
147
    {
2✔
148
        if (fields.Count == 1 && fields[0].FieldName == "*")
2✔
NEW
149
        {
×
NEW
150
            foreach (var kvp in record.DictValue)
×
NEW
151
            {
×
NEW
152
                yield return new KeyValuePair<string, JSONValue>(kvp.Key, kvp.Value);
×
NEW
153
            }
×
154

NEW
155
            yield break;
×
156
        }
157

158
        foreach (var field in fields)
12✔
159
        {
3✔
160
            var fieldName = field.Alias ?? field.FieldName;
3✔
161
            if (record.DictValue.TryGetValue(field.FieldName, out var value))
3✔
162
            {
3✔
163
                yield return new KeyValuePair<string, JSONValue>(fieldName, value);
3✔
164
            }
3✔
165
            else
NEW
166
            {
×
NEW
167
                yield return new KeyValuePair<string, JSONValue>(fieldName, new JSONValue());
×
NEW
168
            }
×
169
        }
3✔
170
    }
2✔
171
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc