All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.tersesystems.echopraxia.scripting.ScriptManager Maven / Gradle / Ivy

package com.tersesystems.echopraxia.scripting;

import static com.tersesystems.echopraxia.scripting.ScriptFunction.*;

import com.tersesystems.echopraxia.api.Level;
import com.tersesystems.echopraxia.api.LoggingContext;
import com.twineworks.tweakflow.lang.TweakFlow;
import com.twineworks.tweakflow.lang.load.loadpath.LoadPath;
import com.twineworks.tweakflow.lang.load.loadpath.MemoryLocation;
import com.twineworks.tweakflow.lang.runtime.Runtime;
import com.twineworks.tweakflow.lang.types.Types;
import com.twineworks.tweakflow.lang.values.*;
import java.util.*;
import java.util.function.Function;

/**
 * ScriptManager class.
 *
 * 

This does the work of evaluating a Tweakflow script from a ScriptHandle. * *

You can add custom and "impure" functions by using addUserFunction. */ public class ScriptManager { public static final Value TRACE_VALUE = Values.make(Level.TRACE.name()); public static final Value DEBUG_VALUE = Values.make(Level.DEBUG.name()); public static final Value INFO_VALUE = Values.make(Level.INFO.name()); public static final Value WARN_VALUE = Values.make(Level.WARN.name()); public static final Value ERROR_VALUE = Values.make(Level.ERROR.name()); private static final List JSON_PARAMETER = Collections.singletonList(new FunctionParameter(0, "jsonPath", Types.STRING, Values.NIL)); private static final FunctionSignature JSON_PATH_BOOLEAN_FUNCTION_SIGNATURE = new FunctionSignature(JSON_PARAMETER, Types.BOOLEAN); private static final FunctionSignature JSON_PATH_LIST_FUNCTION_SIGNATURE = new FunctionSignature(JSON_PARAMETER, Types.LIST); private static final FunctionSignature JSON_PATH_ANY_FUNCTION_SIGNATURE = new FunctionSignature(JSON_PARAMETER, Types.ANY); // () => Any private static final FunctionSignature SUPPLIER_ANY_SIGNATURE = new FunctionSignature(Collections.emptyList(), Types.ANY); private final ScriptHandle handle; private Arity2CallSite callSite; private final Object lock = new Object(); private Function> userFunctions = ctx -> Collections.emptyList(); public ScriptManager(ScriptHandle handle) { this.handle = handle; } public void setUserFunctions(Function> userFunctions) { this.userFunctions = userFunctions; } /* library echopraxia { function evaluate: (string level, dict ctx) -> boolean let { findNumber: ctx[:find_number]; findString: ctx[:find_string]; } findNumber("$.age") == 3 && findString("$.name") == "Will"; } */ public boolean execute(boolean df, Level level, LoggingContext context) { try { Value levelV = getLevelV(level); List functionMapList = new ArrayList<>(userFunctions.apply(context)); addContextFunctions(functionMapList, context); DictValue dictValue = new DictValue(functionMapList.toArray(new ValueMapEntry[0])); Value functionMapValue = Values.make(dictValue); Value retValue = call(levelV, functionMapValue); if (!retValue.isBoolean()) { throw new ScriptException( "Your function needs to return a boolean value! Invalid return type: " + retValue.type()); } return retValue.bool(); } catch (Exception e) { handle.report(e); return df; // pass the default through on exception. } } private Value getLevelV(Level level) { switch (level) { case TRACE: return TRACE_VALUE; case DEBUG: return DEBUG_VALUE; case INFO: return INFO_VALUE; case WARN: return WARN_VALUE; case ERROR: return ERROR_VALUE; default: throw new IllegalStateException("Unknown level " + level); } } private void addContextFunctions(List functionMapList, LoggingContext ctx) { functionMapList.add( ValueMapEntry.make( "fields", new UserFunctionValue( SUPPLIER_ANY_SIGNATURE, (Arity0UserFunction) userCtx -> convertFields(ctx.getFields())))); functionMapList.add( ValueMapEntry.make( "find_number", new UserFunctionValue( JSON_PATH_ANY_FUNCTION_SIGNATURE, optionalFunction(ctx::findNumber)))); functionMapList.add( ValueMapEntry.make( "find_string", new UserFunctionValue( JSON_PATH_ANY_FUNCTION_SIGNATURE, optionalFunction(ctx::findString)))); functionMapList.add( ValueMapEntry.make( "find_boolean", new UserFunctionValue( JSON_PATH_ANY_FUNCTION_SIGNATURE, optionalFunction(ctx::findBoolean)))); functionMapList.add( ValueMapEntry.make( "find_object", new UserFunctionValue( JSON_PATH_ANY_FUNCTION_SIGNATURE, optionalFunction(ctx::findObject)))); functionMapList.add( ValueMapEntry.make( "find_list", new UserFunctionValue(JSON_PATH_LIST_FUNCTION_SIGNATURE, listFunction(ctx::findList)))); functionMapList.add( ValueMapEntry.make( "find_null", new UserFunctionValue( JSON_PATH_BOOLEAN_FUNCTION_SIGNATURE, booleanFunction(ctx::findNull)))); } private Value call(Value level, Value fields) { synchronized (lock) { // if there's no callsite or the handle is bad, we need to eval the script // probably safest to do this in a single thread in synchronized block? // // The handle will only be invalid for _one_ call, so if you get an exception, // it won't try to recompile it again, and you'll get the previous successfully // evaluated call-site next round. // // If the callsite is null and the script is bad, then this is the first time // you've called the script and it WILL keep trying until it works. It // will throw an exception and return the default value so that conditional // logging is not blocked in the meanwhile. if (callSite == null || handle.isInvalid()) { String script = handle.script(); Runtime.Module module = compileModule(script); module.evaluate(); Runtime.Var var = module.getLibrary(handle.libraryName()).getVar(handle.functionName()); callSite = var.arity2CallSite(); } // Callsite is not threadsafe, so only one thread can execute it at a time return callSite.call(level, fields); } } private Runtime.Module compileModule(String script) { String path = handle.path(); MemoryLocation memLocation = new MemoryLocation.Builder().add(path, script).build(); LoadPath loadPath = new LoadPath.Builder().addStdLocation().add(memLocation).build(); Runtime runtime = TweakFlow.compile(loadPath, path); return runtime.getModules().get(runtime.unitKey(path)); } private Arity1UserFunction optionalFunction(Function> contextFunction) { return (context, pathValue) -> { final String path = pathValue.string(); final Optional opt = contextFunction.apply(path); if (opt.isPresent()) { return Values.make(opt.get()); } return Values.NIL; }; } private Arity1UserFunction listFunction(Function> listFunction) { return (context, pathValue) -> { final String path = pathValue.string(); final List list = listFunction.apply(path); return Values.makeList(list); }; } private Arity1UserFunction booleanFunction(Function booleanFunction) { return (context, pathValue) -> { final String path = pathValue.string(); final Boolean result = booleanFunction.apply(path); return Values.make(result); }; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy