Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
/src/.vs
bin
obj
*.user
*.user

# claude code
CLAUDE.md
thoughts/
108 changes: 108 additions & 0 deletions src/Jsonata.Net.Native.Tests/BindingLookupTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using Jsonata.Net.Native;
using Jsonata.Net.Native.Eval;
using Jsonata.Net.Native.Json;
using NUnit.Framework;

namespace Jsonata.Net.Native.Tests
{
public sealed class BindingLookupTests
{
[Test]
public void CanLookupBoundVariable()
{
EvaluationEnvironment env = new EvaluationEnvironment();
env.BindFunction("getVar", GetVariable);
env.BindValue("myVar", new JValue("test value"));

JsonataQuery query = new JsonataQuery("$getVar()");
JToken result = query.Eval(JValue.CreateNull(), env);

Assert.AreEqual("test value", (string)result);
}

[Test]
public void ReturnsUndefinedWhenVariableNotBound()
{
EvaluationEnvironment env = new EvaluationEnvironment();
env.BindFunction("getVar", GetVariable);

JsonataQuery query = new JsonataQuery("$getVar()");
JToken result = query.Eval(JValue.CreateNull(), env);

Assert.AreEqual(JTokenType.Undefined, result.Type);
}

[Test]
public void CanCombineWithRegularParameters()
{
EvaluationEnvironment env = new EvaluationEnvironment();
env.BindFunction("concat", ConcatWithVariable);
env.BindValue("suffix", new JValue(" world"));

JsonataQuery query = new JsonataQuery("$concat('hello')");
JToken result = query.Eval(JValue.CreateNull(), env);

Assert.AreEqual("hello world", (string)result);
}

[Test]
public void CanAccessMultipleVariables()
{
EvaluationEnvironment env = new EvaluationEnvironment();
env.BindFunction("fullName", BuildFullName);
env.BindValue("firstName", new JValue("John"));
env.BindValue("lastName", new JValue("Doe"));

JsonataQuery query = new JsonataQuery("$fullName()");
JToken result = query.Eval(JValue.CreateNull(), env);

Assert.AreEqual("John Doe", (string)result);
}

[Test]
public void ErrorMessageExcludesInjectedParameterFromCount()
{
EvaluationEnvironment env = new EvaluationEnvironment();
env.BindFunction("test", FunctionRequiringOneArg);

JsonataQuery query = new JsonataQuery("$test()");
JsonataException? ex = Assert.Throws<JsonataException>(() =>
query.Eval(JValue.CreateNull(), env));

Assert.AreEqual("T0410", ex!.Code);
Assert.That(ex!.Message, Does.Contain("requires 1"));
}

public static JToken GetVariable(IBindingLookup lookup)
{
return lookup.Lookup("myVar");
}

public static JToken ConcatWithVariable(string prefix, IBindingLookup lookup)
{
JToken suffix = lookup.Lookup("suffix");
if (suffix.Type == JTokenType.String)
{
return new JValue(prefix + (string)suffix);
}
return new JValue(prefix);
}

public static JToken BuildFullName(IBindingLookup lookup)
{
JToken firstName = lookup.Lookup("firstName");
JToken lastName = lookup.Lookup("lastName");

if (firstName.Type == JTokenType.String && lastName.Type == JTokenType.String)
{
return new JValue($"{(string)firstName} {(string)lastName}");
}
return EvalProcessor.UNDEFINED;
}

public static JToken FunctionRequiringOneArg(string required, IBindingLookup lookup)
{
return new JValue("result");
}
}
}
1 change: 0 additions & 1 deletion src/Jsonata.Net.Native/Eval/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ public OptionalArgumentAttribute(object? defaultValue)
}
}

//provides support for builtin functions that require EvaluationSupplement
[AttributeUsage(AttributeTargets.Parameter)]
internal sealed class EvalSupplementArgumentAttribute : Attribute
{
Expand Down
15 changes: 12 additions & 3 deletions src/Jsonata.Net.Native/Eval/FunctionTokenCsharp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal sealed class FunctionTokenCsharp : FunctionToken
private readonly string m_functionName;
private readonly bool m_hasContextParameter;
private readonly bool m_hasEnvParameter;
private readonly bool m_hasBindingLookupParameter;

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

this.RequiredArgsCount = this.m_parameters.Where(p => !p.isOptional && !p.isEvaluationSupplement).Count();
this.RequiredArgsCount = this.m_parameters.Where(p => !p.isOptional && !p.isEvaluationSupplement && !p.isBindingLookup).Count();
}

internal sealed class ArgumentInfo
Expand All @@ -58,6 +60,7 @@ internal sealed class ArgumentInfo
internal readonly bool isOptional;
internal readonly object? defaultValueForOptional;
internal readonly bool isEvaluationSupplement;
internal readonly bool isBindingLookup;
internal readonly bool isVariableArgumentsArray;

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

this.isBindingLookup = parameterInfo.ParameterType == typeof(IBindingLookup);

this.isVariableArgumentsArray = parameterInfo.IsDefined(typeof(VariableNumberArgumentAsArrayAttribute), false);
if (this.isVariableArgumentsArray && parameterInfo.ParameterType != typeof(JArray))
{
Expand Down Expand Up @@ -165,6 +170,10 @@ internal override JToken Invoke(List<JToken> args, JToken? context, EvaluationEn
{
result[targetIndex] = env.GetEvaluationSupplement();
}
else if (argumentInfo.isBindingLookup)
{
result[targetIndex] = env;
}
else if (sourceIndex >= args.Count)
{
if (argumentInfo.isOptional)
Expand All @@ -174,7 +183,7 @@ internal override JToken Invoke(List<JToken> args, JToken? context, EvaluationEn
}
else
{
throw new JsonataException("T0410", $"Function '{m_functionName}' requires {this.m_parameters.Count + (this.m_hasEnvParameter? -1 : 0)} arguments. Passed {args.Count} arguments");
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");
}
}
else if (argumentInfo.isVariableArgumentsArray)
Expand Down Expand Up @@ -202,7 +211,7 @@ internal override JToken Invoke(List<JToken> args, JToken? context, EvaluationEn

if (sourceIndex < args.Count)
{
throw new JsonataException("T0410", $"Function '{m_functionName}' requires {this.m_parameters.Count + (this.m_hasEnvParameter ? -1 : 0)} arguments. Passed {args.Count} arguments");
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");
};

return result;
Expand Down
4 changes: 3 additions & 1 deletion src/Jsonata.Net.Native/EvaluationEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Jsonata.Net.Native
{
public sealed class EvaluationEnvironment
public sealed class EvaluationEnvironment : IBindingLookup
{
public static readonly EvaluationEnvironment DefaultEnvironment;

Expand Down Expand Up @@ -105,6 +105,8 @@ internal JToken Lookup(string name)
}
}

JToken IBindingLookup.Lookup(string name) => this.Lookup(name);

internal EvaluationSupplement GetEvaluationSupplement()
{
if (this.m_evaluationSupplement == null)
Expand Down
24 changes: 24 additions & 0 deletions src/Jsonata.Net.Native/IBindingLookup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Jsonata.Net.Native.Json;

namespace Jsonata.Net.Native
{
/// <summary>
/// Provides access to JSONata bindings (variables and functions) during evaluation.
/// Custom built-in functions can receive this interface via the
/// [BindingLookupArgument] attribute to access bindings in the evaluation environment.
/// </summary>
public interface IBindingLookup
{
/// <summary>
/// Looks up a binding by name in the evaluation environment.
/// Searches the current environment and parent environments
/// hierarchically until found or all environments exhausted.
/// Can return variables, functions, or any bound JToken value.
/// </summary>
/// <param name="name">The binding name to look up (without '$' prefix for variables/functions)</param>
/// <returns>
/// The JToken value bound to the name (can be a value, function, or UNDEFINED if not found)
/// </returns>
JToken Lookup(string name);
}
}