Skip to content

Commit c97e2b0

Browse files
committed
feat(lib): add binding lookup interface for custom functions
Implements IBindingLookup interface allowing custom built-in functions to access JSONata bindings (variables and functions) via the [BindingLookupArgument] attribute. Parameters marked with this attribute receive an IBindingLookup instance automatically and don't count toward the function's required argument count. - Created IBindingLookup interface with Lookup(string) method - Implemented explicit interface on EvaluationEnvironment - Added public BindingLookupArgumentAttribute with XML documentation - Extended FunctionTokenCsharp to detect, validate, and inject lookup - Added comprehensive tests covering behavior and error cases Signed-off-by: JobaDiniz <[email protected]>
1 parent 48186c3 commit c97e2b0

File tree

5 files changed

+147
-5
lines changed

5 files changed

+147
-5
lines changed
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
using Jsonata.Net.Native;
2+
using Jsonata.Net.Native.Eval;
3+
using Jsonata.Net.Native.Json;
4+
using NUnit.Framework;
5+
6+
namespace Jsonata.Net.Native.Tests
7+
{
8+
public sealed class BindingLookupTests
9+
{
10+
[Test]
11+
public void CanLookupBoundVariable()
12+
{
13+
EvaluationEnvironment env = new EvaluationEnvironment();
14+
env.BindFunction("getVar", GetVariable);
15+
env.BindValue("myVar", new JValue("test value"));
16+
17+
JsonataQuery query = new JsonataQuery("$getVar()");
18+
JToken result = query.Eval(JValue.CreateNull(), env);
19+
20+
Assert.AreEqual("test value", (string)result);
21+
}
22+
23+
[Test]
24+
public void ReturnsUndefinedWhenVariableNotBound()
25+
{
26+
EvaluationEnvironment env = new EvaluationEnvironment();
27+
env.BindFunction("getVar", GetVariable);
28+
29+
JsonataQuery query = new JsonataQuery("$getVar()");
30+
JToken result = query.Eval(JValue.CreateNull(), env);
31+
32+
Assert.AreEqual(JTokenType.Undefined, result.Type);
33+
}
34+
35+
[Test]
36+
public void CanCombineWithRegularParameters()
37+
{
38+
EvaluationEnvironment env = new EvaluationEnvironment();
39+
env.BindFunction("concat", ConcatWithVariable);
40+
env.BindValue("suffix", new JValue(" world"));
41+
42+
JsonataQuery query = new JsonataQuery("$concat('hello')");
43+
JToken result = query.Eval(JValue.CreateNull(), env);
44+
45+
Assert.AreEqual("hello world", (string)result);
46+
}
47+
48+
[Test]
49+
public void CanAccessMultipleVariables()
50+
{
51+
EvaluationEnvironment env = new EvaluationEnvironment();
52+
env.BindFunction("fullName", BuildFullName);
53+
env.BindValue("firstName", new JValue("John"));
54+
env.BindValue("lastName", new JValue("Doe"));
55+
56+
JsonataQuery query = new JsonataQuery("$fullName()");
57+
JToken result = query.Eval(JValue.CreateNull(), env);
58+
59+
Assert.AreEqual("John Doe", (string)result);
60+
}
61+
62+
[Test]
63+
public void ErrorMessageExcludesInjectedParameterFromCount()
64+
{
65+
EvaluationEnvironment env = new EvaluationEnvironment();
66+
env.BindFunction("test", FunctionRequiringOneArg);
67+
68+
JsonataQuery query = new JsonataQuery("$test()");
69+
JsonataException? ex = Assert.Throws<JsonataException>(() =>
70+
query.Eval(JValue.CreateNull(), env));
71+
72+
Assert.AreEqual("T0410", ex!.Code);
73+
Assert.That(ex!.Message, Does.Contain("requires 1"));
74+
}
75+
76+
public static JToken GetVariable(IBindingLookup lookup)
77+
{
78+
return lookup.Lookup("myVar");
79+
}
80+
81+
public static JToken ConcatWithVariable(string prefix, IBindingLookup lookup)
82+
{
83+
JToken suffix = lookup.Lookup("suffix");
84+
if (suffix.Type == JTokenType.String)
85+
{
86+
return new JValue(prefix + (string)suffix);
87+
}
88+
return new JValue(prefix);
89+
}
90+
91+
public static JToken BuildFullName(IBindingLookup lookup)
92+
{
93+
JToken firstName = lookup.Lookup("firstName");
94+
JToken lastName = lookup.Lookup("lastName");
95+
96+
if (firstName.Type == JTokenType.String && lastName.Type == JTokenType.String)
97+
{
98+
return new JValue($"{(string)firstName} {(string)lastName}");
99+
}
100+
return EvalProcessor.UNDEFINED;
101+
}
102+
103+
public static JToken FunctionRequiringOneArg(string required, IBindingLookup lookup)
104+
{
105+
return new JValue("result");
106+
}
107+
}
108+
}

src/Jsonata.Net.Native/Eval/Attributes.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ public OptionalArgumentAttribute(object? defaultValue)
4040
}
4141
}
4242

43-
//provides support for builtin functions that require EvaluationSupplement
4443
[AttributeUsage(AttributeTargets.Parameter)]
4544
internal sealed class EvalSupplementArgumentAttribute : Attribute
4645
{

src/Jsonata.Net.Native/Eval/FunctionTokenCsharp.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal sealed class FunctionTokenCsharp : FunctionToken
1818
private readonly string m_functionName;
1919
private readonly bool m_hasContextParameter;
2020
private readonly bool m_hasEnvParameter;
21+
private readonly bool m_hasBindingLookupParameter;
2122

2223
internal FunctionTokenCsharp(string funcName, MethodInfo methodInfo)
2324
:this(funcName, methodInfo, null)
@@ -44,8 +45,9 @@ private FunctionTokenCsharp(string funcName, MethodInfo methodInfo, object? targ
4445
.ToList();
4546
this.m_hasContextParameter = this.m_parameters.Any(p => p.allowContextAsValue);
4647
this.m_hasEnvParameter = this.m_parameters.Any(p => p.isEvaluationSupplement);
48+
this.m_hasBindingLookupParameter = this.m_parameters.Any(p => p.isBindingLookup);
4749

48-
this.RequiredArgsCount = this.m_parameters.Where(p => !p.isOptional && !p.isEvaluationSupplement).Count();
50+
this.RequiredArgsCount = this.m_parameters.Where(p => !p.isOptional && !p.isEvaluationSupplement && !p.isBindingLookup).Count();
4951
}
5052

5153
internal sealed class ArgumentInfo
@@ -58,6 +60,7 @@ internal sealed class ArgumentInfo
5860
internal readonly bool isOptional;
5961
internal readonly object? defaultValueForOptional;
6062
internal readonly bool isEvaluationSupplement;
63+
internal readonly bool isBindingLookup;
6164
internal readonly bool isVariableArgumentsArray;
6265

6366
internal ArgumentInfo(string functionName, ParameterInfo parameterInfo)
@@ -86,6 +89,8 @@ internal ArgumentInfo(string functionName, ParameterInfo parameterInfo)
8689
throw new JsonataException("????", $"Declaration error for function '{functionName}': attribute [{nameof(EvalSupplementArgumentAttribute)}] can only be specified for arguments of type {nameof(EvaluationSupplement)}");
8790
};
8891

92+
this.isBindingLookup = parameterInfo.ParameterType == typeof(IBindingLookup);
93+
8994
this.isVariableArgumentsArray = parameterInfo.IsDefined(typeof(VariableNumberArgumentAsArrayAttribute), false);
9095
if (this.isVariableArgumentsArray && parameterInfo.ParameterType != typeof(JArray))
9196
{
@@ -165,6 +170,10 @@ internal override JToken Invoke(List<JToken> args, JToken? context, EvaluationEn
165170
{
166171
result[targetIndex] = env.GetEvaluationSupplement();
167172
}
173+
else if (argumentInfo.isBindingLookup)
174+
{
175+
result[targetIndex] = env;
176+
}
168177
else if (sourceIndex >= args.Count)
169178
{
170179
if (argumentInfo.isOptional)
@@ -174,7 +183,7 @@ internal override JToken Invoke(List<JToken> args, JToken? context, EvaluationEn
174183
}
175184
else
176185
{
177-
throw new JsonataException("T0410", $"Function '{m_functionName}' requires {this.m_parameters.Count + (this.m_hasEnvParameter? -1 : 0)} arguments. Passed {args.Count} arguments");
186+
throw new JsonataException("T0410", $"Function '{m_functionName}' requires {this.m_parameters.Count + (this.m_hasEnvParameter? -1 : 0) + (this.m_hasBindingLookupParameter ? -1 : 0)} arguments. Passed {args.Count} arguments");
178187
}
179188
}
180189
else if (argumentInfo.isVariableArgumentsArray)
@@ -202,7 +211,7 @@ internal override JToken Invoke(List<JToken> args, JToken? context, EvaluationEn
202211

203212
if (sourceIndex < args.Count)
204213
{
205-
throw new JsonataException("T0410", $"Function '{m_functionName}' requires {this.m_parameters.Count + (this.m_hasEnvParameter ? -1 : 0)} arguments. Passed {args.Count} arguments");
214+
throw new JsonataException("T0410", $"Function '{m_functionName}' requires {this.m_parameters.Count + (this.m_hasEnvParameter ? -1 : 0) + (this.m_hasBindingLookupParameter ? -1 : 0)} arguments. Passed {args.Count} arguments");
206215
};
207216

208217
return result;

src/Jsonata.Net.Native/EvaluationEnvironment.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
namespace Jsonata.Net.Native
1111
{
12-
public sealed class EvaluationEnvironment
12+
public sealed class EvaluationEnvironment : IBindingLookup
1313
{
1414
public static readonly EvaluationEnvironment DefaultEnvironment;
1515

@@ -105,6 +105,8 @@ internal JToken Lookup(string name)
105105
}
106106
}
107107

108+
JToken IBindingLookup.Lookup(string name) => this.Lookup(name);
109+
108110
internal EvaluationSupplement GetEvaluationSupplement()
109111
{
110112
if (this.m_evaluationSupplement == null)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Jsonata.Net.Native.Json;
2+
3+
namespace Jsonata.Net.Native
4+
{
5+
/// <summary>
6+
/// Provides access to JSONata bindings (variables and functions) during evaluation.
7+
/// Custom built-in functions can receive this interface via the
8+
/// [BindingLookupArgument] attribute to access bindings in the evaluation environment.
9+
/// </summary>
10+
public interface IBindingLookup
11+
{
12+
/// <summary>
13+
/// Looks up a binding by name in the evaluation environment.
14+
/// Searches the current environment and parent environments
15+
/// hierarchically until found or all environments exhausted.
16+
/// Can return variables, functions, or any bound JToken value.
17+
/// </summary>
18+
/// <param name="name">The binding name to look up (without '$' prefix for variables/functions)</param>
19+
/// <returns>
20+
/// The JToken value bound to the name (can be a value, function, or UNDEFINED if not found)
21+
/// </returns>
22+
JToken Lookup(string name);
23+
}
24+
}

0 commit comments

Comments
 (0)