-
Notifications
You must be signed in to change notification settings - Fork 8
feat: add binding lookup interface for custom functions #44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
feat: add binding lookup interface for custom functions #44
Conversation
Excludes thoughts/ directory and CLAUDE.md file.
274ea5d to
d211dc0
Compare
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]>
d211dc0 to
c97e2b0
Compare
|
Hi @JobaDiniz thank you for the proposal! I seem to get the general idea of your proposal, but I'm not sure if it should be implemented this way. What worries me is that your solution is specific to our dotnet implementation of the JSONata language, which may be not ideal. Therefore I suggest that you propose such functionality to be added to the JSONata language itself (for example - via a new system function that returns a value given a name, i.e. What do you think? |
|
Also, have you seen the Something like this: JObject bindings = new JObject();
bindings.Add("headerText", new JValue("Title"));
env.BindValue("bindings", bindings);and then have a jsonata function |
|
TLDR: Our C# custom function processes Draft.js rich text structures that contain nested variable references. During C# runtime processing (not JSONata evaluation), we need to look up those nested variables from the JSONata bindings to resolve their values. The Problem in DetailWe work with Draft.js rich text structures that can reference other variables. Our C# custom function needs to resolve these nested references by looking them up in the JSONata environment. Complete Example with Actual ValuesAll variables bound in JSONata environment: env.BindValue("___f", new JValue(42)); // number
env.BindValue("___d", new JValue(8)); // number
env.BindValue("report", JToken.Parse(...)); // Draft.js structure (see below)The {
"blocks": [{
"text": "The sum is ___f and difference is ___d",
"entityRanges": [
{ "key": 0, "offset": 11, "length": 4 },
{ "key": 1, "offset": 35, "length": 4 }
]
}],
"entityMap": {
"0": {
"type": "VARIABLE",
"data": { "variableName": "___f" }
},
"1": {
"type": "VARIABLE",
"data": { "variableName": "___d" }
}
}
}JSONata expression: $draftjsFormat($report)The Transformation FlowStep 1: JSONata evaluates Step 2: JSONata calls our custom C# function Step 3: Inside our C# code, we parse the structure: // We're now in C# runtime code, not JSONata evaluation
foreach (var block in definition.Blocks)
{
var text = block.Text; // "The sum is ___f and difference is ___d"
foreach (var entityRange in block.EntityRanges)
{
var entityType = GetEntityType(entityRange, entityMap); // "VARIABLE"
var variableName = GetVariableName(entityRange, entityMap); // "___f"
// HERE'S THE PROBLEM:
// We're in C# code and need to get the value of "___f" from bindings
// We can't write a JSONata expression here - this is C# runtime
var value = lookup.Lookup(variableName); // Gets JValue(42)
// Convert to string and substitute in text
text = ReplaceRange(text, entityRange.Offset, entityRange.Length, "42");
}
}Step 4: After processing all nested variables: Step 5: Convert to markdown with formatting, return to JSONata Why We Need IBindingLookup
Why $lookup() Doesn't Work
$lookup($myObject, "propertyName")But we need to:
The situation is analogous to: imagine if a custom function needed to generate a random number based on data in the structure it's processing - it would use Implementation ScopeThis is a .NET-specific extension for custom function authors who need to:
It doesn't change JSONata language semantics - it extends what custom C# functions can do, similar to how Would you be open to this as a capability for custom function implementations? Happy to discuss alternative approaches if you have suggestions! |
|
@JobaDiniz thanks for a detailed explanation! I do agree now that your proposal is out of Jsonata language spec scope. Though I think there's a way it could be done in a bit another way like this: All variables bound in JSONata environment via single JObject named e.g. JObject vars = new JObject();
vars.Add("___f", new JValue(42))
vars.Add("___d", new JValue(8))
env.BindValue("vars", vars);
env.BindValue("report", JToken.Parse(...)); JSONata expression - pass inside C# code: public static JToken draftjsFormat(JToken repors, JToken vars)
{
Dictionary<string, JToken> varsLookup = ((JObject)vars).ChildrenTokens;
...
// We're now in C# runtime code, not JSONata evaluation
foreach (var block in definition.Blocks)
{
var text = block.Text; // "The sum is ___f and difference is ___d"
foreach (var entityRange in block.EntityRanges)
{
var entityType = GetEntityType(entityRange, entityMap); // "VARIABLE"
var variableName = GetVariableName(entityRange, entityMap); // "___f"
// HERE:
var value = varsLookup[variableName]; // Gets JValue(42)
// Convert to string and substitute in text
text = ReplaceRange(text, entityRange.Offset, entityRange.Length, "42");
}
}What do you think, will this do? |
|
I think there might be a misunderstanding about what we're trying to achieve. The variables are already bound in the evaluation environment - we're not asking to pass new data. Current State (What We Already Do)All variables are bound in the JSONata environment: env.BindValue("___f", new JValue(42));
env.BindValue("___d", new JValue(8));
env.BindValue("report", JToken.Parse(...));JSONata expressions can access these bindings: $___f // Returns 42
$___d // Returns 8
$report // Returns the Draft.js structureThe ProblemWhen our C# custom function runs, it needs to resolve nested variable references found inside the data structure it's processing. But there's no API for C# code to access these bindings. Your Proposed WorkaroundYour workaround would require passing the same data twice: // First: bind all variables (already doing this)
env.BindValue("___f", new JValue(42));
env.BindValue("___d", new JValue(8));
env.BindValue("report", JToken.Parse(...));
// Second: create a JObject with the SAME data
JObject vars = new JObject();
vars.Add("___f", new JValue(42)); // Duplicating ___f
vars.Add("___d", new JValue(8)); // Duplicating ___d
env.BindValue("vars", vars); // Passing data a second time
// Now we have the same data in two places:
// 1. As individual bindings: $___f, $___d
// 2. As properties in $vars: $vars.___f, $vars.___dThen every function call needs to explicitly pass this duplicated data: $draftjsFormat($report, $vars) // Passing $vars explicitlyThis seems redundant - we're passing data that's already in the binding context. What We're Actually Asking ForWe're asking for C# custom functions to access what's already there. The binding context exists, it's already populated, we just need an API to access it from C# code. This is similar to how The Core QuestionWhy shouldn't C# custom functions have access to the binding context they're running in?
The alternative (passing data twice - once in bindings, once as parameters) seems like a workaround for a missing API, not a proper solution. What's the concern with providing direct access to what's already in the context? |
|
Hi @JobaDiniz and sorry for a late answer.
I am not totally against allowing access for c# function to the binding environment. I think it's a valid feature. The thing is that I'm currently working on a global rewrite of Jsonata.Net.Native (see new_attempt branch), which is inspired by the recent java port. The goal is to make our implementation much more complete in regard to the reference implementation — stuff like The change is rather significant and will surely be a breaking one (at least in regard to introspection api). I will try to make the transition as easy as possible, but right now I'm focused on the core implementation features. So while I'm at it, I'm not very inclined to add features to the current implementation. For example, I'm not sure I'd like to introduce the new This is why I'm suggesting you to try a workaround that would work right now, without any changes to the library (and would work later as well). You say:
But why? Let's see once again. Your code from above: // First: bind all variables (already doing this)
env.BindValue("___f", new JValue(42));
env.BindValue("___d", new JValue(8));
env.BindValue("report", JToken.Parse(...));don't do this ^^^ // Second: create a JObject with the SAME data
JObject vars = new JObject();
vars.Add("___f", new JValue(42)); // Duplicating ___f
vars.Add("___d", new JValue(8)); // Duplicating ___d
env.BindValue("vars", vars); // Passing data a second timejust do this ^^^ The only potential problem I see there, is that if you are using the vars in other parts of query as is, you'll cave to change them from |
|
Once again, I agree that my proposal is more a hack/workaround than proper API, and I'd like to add the proper API, but I'd prefer to do this after or during v3 release. |
|
Why are you rewriting? |
as I wrote above — because current version has a number of not implemented features, that you may find here (eg, So I decided that sticking more to the reference implementation would solve a number of existing problems, and also will simplify things in future, when new features added to the reference. So while it's a significant amount of work, it seems that the main part is completed already. At the moment of writing, the new implementation passes 1527 tests of the 1655 in the reference test-suite, while the old one had 1267. So seems that I'm getting near. |
Summary
Adds a new public interface
IBindingLookupthat allows custom built-in functions to access JSONata bindings (variables and functions) during evaluation.Use Case
This enables advanced scenarios where custom functions need to interact with the evaluation context. For example, when working with complex data structures like Draft.js that define their own variable systems, custom functions can now map those internal variables to JSONata bindings dynamically.
Usage Example
Implementation
IBindingLookupinterface withLookup(string name)methodIBindingLookupare automatically detected and injectedBreaking Changes
None - this is a purely additive feature.