Skip to content

Commit 23fb931

Browse files
committed
Refactor ContextResolver and EvaluatorResolver to support multiple context references
1 parent 001faa6 commit 23fb931

File tree

4 files changed

+65
-21
lines changed

4 files changed

+65
-21
lines changed

expression-src/main/src/interpreter/ContextResolver.cls

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
11
public with sharing class ContextResolver implements Visitor {
22
private Boolean shouldExecuteQuery = false;
33
private Query queryContext;
4-
private final String contextReferenceName;
4+
private final Set<String> contextReferenceNames;
55

66
private final List<Expr.FunctionDeclaration> customFunctionDeclarations;
77
private final Query topLevelQuery;
88
private final List<Query> subQueries = new List<Query>();
99

10-
public ContextResolver(Id recordId, List<Expr.FunctionDeclaration> customFunctionDeclarations, String contextReferenceName) {
10+
public ContextResolver(Id recordId, List<Expr.FunctionDeclaration> customFunctionDeclarations, Set<String> contextReferenceNames) {
1111
this.queryContext = new Query(recordId);
1212
this.topLevelQuery = this.queryContext;
1313
this.customFunctionDeclarations = customFunctionDeclarations;
14-
this.contextReferenceName = contextReferenceName;
14+
this.contextReferenceNames = contextReferenceNames;
1515
}
1616

17-
public ContextResolver(Set<Id> recordIds, List<Expr.FunctionDeclaration> customFunctionDeclarations, String contextReferenceName) {
17+
public ContextResolver(Set<Id> recordIds, List<Expr.FunctionDeclaration> customFunctionDeclarations, Set<String> contextReferenceNames) {
1818
this.queryContext = new Query(recordIds);
1919
this.topLevelQuery = this.queryContext;
2020
this.customFunctionDeclarations = customFunctionDeclarations;
21-
this.contextReferenceName = contextReferenceName;
21+
this.contextReferenceNames = contextReferenceNames;
2222
}
2323

2424
private Object resolve(Expr expr) {
@@ -227,7 +227,7 @@ public with sharing class ContextResolver implements Visitor {
227227

228228
private Boolean isReferencingContextField(Expr.GetExpr expr) {
229229
if (expr.objectExpr instanceof Expr.Variable) {
230-
return ((Expr.Variable)expr.objectExpr).isContext(this.contextReferenceName);
230+
return ((Expr.Variable)expr.objectExpr).isContext(this.contextReferenceNames);
231231
} else if (expr.objectExpr instanceof Expr.GetExpr) {
232232
// Recursively check if the parent object is referencing a context field
233233
// since we can have nested get expressions and what is important is the top level
@@ -238,7 +238,7 @@ public with sharing class ContextResolver implements Visitor {
238238
}
239239

240240
public Object visit(Expr.Variable variable) {
241-
if (variable.isContext(this.contextReferenceName)) {
241+
if (variable.isContext(this.contextReferenceNames)) {
242242
// If context is being accessed, then we always want to run the query.
243243
this.shouldExecuteQuery = true;
244244

expression-src/main/src/interpreter/Expr.cls

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,14 @@ public abstract class Expr {
165165
return v.visit(this);
166166
}
167167

168-
public Boolean isContext(String contextReferenceName) {
169-
return this.name.lexeme.toLowerCase() == contextReferenceName.toLowerCase();
168+
public Boolean isContext(Set<String> contextReferenceNames) {
169+
String lowercasedLexeme = this.name.lexeme.toLowerCase();
170+
for (String contextReferenceName : contextReferenceNames) {
171+
if (lowercasedLexeme == contextReferenceName.toLowerCase()) {
172+
return true;
173+
}
174+
}
175+
return false;
170176
}
171177
}
172178

expression-src/main/src/resolver/EvaluatorResolver.cls

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public with sharing abstract class EvaluatorResolver {
4242
return null;
4343
}
4444

45-
ContextResolver contextResolver = new ContextResolver(recordId, new List<Expr.FunctionDeclaration>(), '@' + DEFAULT_CONTEXT_KEY);
45+
ContextResolver contextResolver = new ContextResolver(recordId, new List<Expr.FunctionDeclaration>(), new Set<String> { '@' + DEFAULT_CONTEXT_KEY });
4646

4747
for (String formula : formulas) {
4848
try {
@@ -191,7 +191,8 @@ public with sharing abstract class EvaluatorResolver {
191191

192192
public override Environment prepareEnvironment(List<Expr> expressions,
193193
List<Expr.FunctionDeclaration> customFunctionDeclarations, String contextPrefix) {
194-
ContextResolver ctxInterpreter = new ContextResolver(recordId, customFunctionDeclarations, contextPrefix + DEFAULT_CONTEXT_KEY);
194+
ContextResolver ctxInterpreter = new ContextResolver(recordId, customFunctionDeclarations,
195+
new Set<String> { contextPrefix + DEFAULT_CONTEXT_KEY });
195196
List<SObject> records = ctxInterpreter.build(expressions);
196197
SObject record;
197198
if (records != null && records.size() > 0) {
@@ -214,7 +215,8 @@ public with sharing abstract class EvaluatorResolver {
214215
}
215216

216217
public override Environment prepareEnvironment(List<Expr> expressions, List<Expr.FunctionDeclaration> customFunctionDeclarations, String contextPrefix) {
217-
ContextResolver ctxInterpreter = new ContextResolver(recordIds, customFunctionDeclarations, contextPrefix + DEFAULT_CONTEXT_KEY);
218+
ContextResolver ctxInterpreter = new ContextResolver(recordIds, customFunctionDeclarations,
219+
new Set<String> { contextPrefix + DEFAULT_CONTEXT_KEY });
218220
List<SObject> records = ctxInterpreter.build(expressions);
219221
// The environment is created without a record, as we are dealing with a list of records,
220222
// so it would not be possible to interpret references to record fields. Instead, the @context
@@ -237,15 +239,35 @@ public with sharing abstract class EvaluatorResolver {
237239

238240
public override Environment prepareEnvironment(List<Expr> expressions, List<Expr.FunctionDeclaration> customFunctionDeclarations, String contextPrefix) {
239241
Environment env = new Environment();
242+
243+
Map<SObjectType, List<CustomRecordContext>> contextsBySObjectTypes = new Map<SObjectType, List<CustomRecordContext>>();
240244
for (CustomRecordContext context : this.contexts) {
241-
ContextResolver ctxInterpreter = new ContextResolver(context.recordId, customFunctionDeclarations, contextPrefix + context.key);
242-
List<SObject> records = ctxInterpreter.build(expressions);
243-
SObject record;
244-
if (records != null && records.size() > 0) {
245-
record = records[0];
245+
SObjectType sObjectType = context.recordId.getSobjectType();
246+
if (!contextsBySObjectTypes.containsKey(sObjectType)) {
247+
contextsBySObjectTypes.put(sObjectType, new List<CustomRecordContext>());
248+
}
249+
contextsBySObjectTypes.get(sObjectType).add(context);
250+
}
251+
252+
for (SObjectType currentType : contextsBySObjectTypes.keySet()) {
253+
List<CustomRecordContext> contextsForType = contextsBySObjectTypes.get(currentType);
254+
Set<Id> idsForType = new Set<Id>();
255+
Set<String> keys = new Set<String>();
256+
Map<Id, CustomRecordContext> contextByRecordId = new Map<Id, CustomRecordContext>();
257+
for (CustomRecordContext context : contextsForType) {
258+
contextByRecordId.put(context.recordId, context);
259+
idsForType.add(context.recordId);
260+
keys.add(contextPrefix + context.key);
246261
}
247262

248-
Environment.addGlobalContextVariable(contextPrefix, context.key, record);
263+
ContextResolver ctxInterpreter = new ContextResolver(idsForType, customFunctionDeclarations, keys);
264+
Map<Id, SObject> recordById = new Map<Id, SObject>(ctxInterpreter.build(expressions));
265+
for (Id recordId : recordById.keySet()) {
266+
SObject record = recordById.get(recordId);
267+
CustomRecordContext context = contextByRecordId.get(recordId);
268+
269+
Environment.addGlobalContextVariable(contextPrefix, context.key, record);
270+
}
249271
}
250272

251273
return env;

expression-src/spec/language/global-context/GlobalContextTest.cls

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -270,9 +270,25 @@ private class GlobalContextTest {
270270
Assert.areEqual(2, Limits.getQueries(), 'Expect one query per SObject type');
271271
}
272272

273-
// TODO: Many record contexts for different SObject types -> expect one query per type
274-
// TODO: Many record contexts for the same SObject type -> expect to be bulk
275-
// TODO: Validate that the config cannot receive mroe than one custom record context with the same name
273+
@IsTest
274+
private static void canCreateCustomContextsBasedOnASpecifiedIdInBulk_sameSObjectTypes() {
275+
Account accountRecord1 = new Account(Name = 'Acme', NumberOfEmployees = 50);
276+
insert accountRecord1;
277+
278+
Account accountRecord2 = new Account(Name = 'Beta', NumberOfEmployees = 100);
279+
insert accountRecord2;
280+
281+
CustomRecordContext accountRecordContext = new CustomRecordContext('FirstAccount', accountRecord1.Id);
282+
CustomRecordContext contactRecordContext = new CustomRecordContext('SecondAccount', accountRecord2.Id);
283+
284+
String expressionFormula = '@FirstAccount.NumberOfEmployees + @SecondAccount.NumberOfEmployees';
285+
Object result = Evaluator.run(expressionFormula,
286+
new List<CustomRecordContext> { accountRecordContext, contactRecordContext }, new Configuration());
287+
288+
Assert.areEqual(150, result);
289+
Assert.areEqual(1, Limits.getQueries(), 'Expect one query per SObject type');
290+
}
291+
276292
// TODO: Endpoint can received a prequeried SObject on top of the ID. If all of the fields have
277293
// already been queried, then no need to query again.
278294
}

0 commit comments

Comments
 (0)