Skip to content

Commit d211dc0

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 d211dc0

File tree

5 files changed

+160
-5
lines changed

5 files changed

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

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,23 @@ 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
{
4746
}
4847

48+
/// <summary>
49+
/// Marks a parameter to receive an <see cref="IBindingLookup"/> instance that provides
50+
/// access to JSONata bindings (variables and functions) during evaluation.
51+
/// The parameter type must be <see cref="IBindingLookup"/>.
52+
/// This parameter is automatically injected and does not count toward the function's
53+
/// required argument count in JSONata expressions.
54+
/// </summary>
55+
[AttributeUsage(AttributeTargets.Parameter)]
56+
public sealed class BindingLookupArgumentAttribute : Attribute
57+
{
58+
}
59+
4960
[AttributeUsage(AttributeTargets.Parameter)]
5061
internal sealed class VariableNumberArgumentAsArrayAttribute : Attribute
5162
{

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)