Logical Processor Abstraction

Related to my prior post about using server-side JavaScript for Opti event handling, I’ve been wondering lately about abstracting the idea of “logical processing.” If we were to forget about the implementation for a second, what would an abstraction of logic look like?

To experiment with it, I decided that my logical processor would filter a key/value dictionary of data – meaning we’d give it a dictionary, and get one back. The values in that might have changes, or they might not. We don’t really care, we’d just be prepared to use whatever we got back.

I wrapped the dictionary in another class, just to provide some convenience methods, and came up with this for a simple abstraction:

public interface ILogicalProcesser
{
  public DataPayload Execute(string code, DataPayload data);
}

(DataPayload is just an extension of Dictionary<string, object>.)

Then, I created two implementations – one for Fluid and one for Jint, which is a JavaScript processor in C#.

Here’s the Jint version:

public class JintLogicalProcessor : ILogicalProcesser
{
  public DataPayload Execute(string code, DataPayload data)
  {
    var engine = new Engine();
    foreach (var i in data)
    {
      engine.SetValue(i.Key, i.Value);
    }
    engine.Execute(code);
    foreach (var i in data)
    {
      data[i.Key] = engine.GetValue(i.Key).AsString();
    }
    return data;
  }
}

This is quite clean and basically mirrored what I did before. As I noted back then, there’s a bunch of ways to get data in and out of Jint, and I’m not claiming this is the best, but it works.

Here’s the same thing in Fluid.

public class FluidLogicalProcessor : ILogicalProcesser
{
private FluidParser p = new FluidParser();

  public DataPayload Execute(string code, DataPayload data)
  {
    code = string.Concat("{% liquid ", code, " %}\n");
    foreach (var i in data)
    {
      code = code + $"{{% capture {i.Key} %}}{{{{{i.Key}}}}}{{% endcapture %}}\n";
    }

    var template = p.Parse(code);

    var c = new TemplateContext();
    foreach (var i in data)
    {
      c.SetValue(i.Key, i.Value);
    }

    c.Captured = (i, v) =>
    {
      data[i] = v;
      return new ValueTask<string>();
    }

    template.Render(c);
    return data;
  }
}

Now, there’s some serious cruft in there –

First, I’m faking up a template. Fluid has a liquid tag which allows you to write Liquid script without tags. I’m surrounding the provided source code with one of these.

Second, Fluid has a Captured delegate that will execute whenever the capture tag is invoked. However, it doesn’t do the same thing for assign. In another project, I altered the Fluid code to do this on assign (it involved pasting 2-3 lines of code), but I wanted to do this with default Fluid, so you can see I actually concatenate a bunch of capture tags at the bottom. Those tags invoke the delegate which write back to the dictionary.

(I ran this concept of Fluid as a general procedural language by Sebastian, the creator of Fluid. He’s a little horrified by it, since this very much not what Fluid is designed for, but he conceded that it was workable.)

The tests looked like this:

[TestMethod]
public void FluidVariablesShouldReassign()
{
  ILogicalProcesser lp = new FluidLogicalProcessor();
  var code = "assign name = 'Annie'";
  var data = new DataPayload("name", "Deane");
  lp.Execute(code, data);

  Assert.AreEqual("Annie", data.First().Value);
}

[TestMethod]
public void JintVariablesShouldReassign()
{
  ILogicalProcesser lp = new JintLogicalProcessor();
  var code = "var name = 'Annie'";
  var data = new DataPayload("name", "Deane");
  lp.Execute(code, data);
  Assert.AreEqual("Annie", data.First().Value);
}

This may not look like much, but the idea is that the points in your software that need logical processing (event handlers, for example), could simple inject the service. Like this:

var code = "[source code you got from somewhere else]";
var myData = "Deane";

var lp ServiceLocator.Current.GetInstance<ILogicalProcessor>();
var data = new DataPayload("value", myData);
lp.Execute(code, myData)

myData = data.Get<string>("value");

The larger question here is: how do we abstract logical processing?

This implementation provides the ability to filter dictionary data, and is that enough? …maybe? I was thinking about a method for Calculate<T>, so you could do something like this:

var result = lp.Calculate<bool>(code, data);

You can do this with the dictionary method, it just gets a little cumbersome.

There would also need to be a TryParse method for when the code was being created (like in a web-based IDE). It would return a boolean, and – hopefully, depending on the library – some contextual information about where the error was.

The next step is to attempt to implement this abstraction in an actual application and see where it comes up short.