Rules and rule templates YAML integration

To make it “more user-friendly”, we made a couple of aliases classes for the YAML:

The use of the aliases is optional, so it will work just as well with the full MIME type.

| and > are both YAML, and I don’t quite remember the difference. It’s something with how newlines are treated I believe. I took those examples from the tests I have made for the parser, where it is a point to test “every conceivable combination”, so these examples aren’t made to be “pedagogic” or show “best practices”.

By the way, on the topic of shared context, @florian-h05 made some additions to the helper library not that long ago that I doubt most are aware of, making it possible to make “non SimpleRule” rules from JS in an easy way:

const { rules, triggers, utils, cache } = require('openhab')

function myFunction() {
  console.log('myFunction()')
}

rules.JSRule({
  name: 'Shared Context Rule',
  execute: (event) => {
    myFunction()
    utils.dumpObject(this)
  }
})

rules.JSRule({
  name: 'Dedicated Context Rule',
  triggers: triggers.ItemCommandTrigger('test_switch'),
  execute: (event) => {
    myFunction() // expect this to fail
    utils.dumpObject(this)
    java.lang.Thread.sleep(5000)
  },
  dedicatedContext: true
})

rules.JSRule({
  name: 'Cache Test',
  execute: (event) => {
    cache.shared.remove('obj');
    let obj = cache.shared.get('obj', () => {
      let LocalDateTimeClass = Java.type('java.time.LocalDateTime');
      return {
      name: 'Complex Object',
      rating: 5.2,
      tags: ['tag1', 'tag2'],
      created: new Date(),
      javaObj: LocalDateTimeClass.now(),
      nested: { a: 1, b: 2 },
      level: 15
    }});
    utils.dumpObject(obj);
  }
})

When parsed, this rule looks like this:

{
  "status": {
    "status": "IDLE",
    "statusDetail": "NONE"
  },
  "editable": false,
  "triggers": [
    {
      "id": "0",
      "configuration": {
        "itemName": "MySwitch"
      },
      "type": "core.ItemCommandTrigger"
    }
  ],
  "conditions": [],
  "actions": [
    {
      "inputs": {},
      "id": "script",
      "configuration": {
        "type": "application/vnd.openhab.dsl.rule",
        "script": "// context: shared_test-1\nlogInfo('test', \"Global variable is \" + globalVariable);\n"
      },
      "type": "script.ScriptAction"
    }
  ],
  "configuration": {},
  "configDescriptions": [],
  "templateState": "no-template",
  "uid": "shared_test-1",
  "name": "Test rule",
  "tags": [],
  "visibility": "VISIBLE"
}

Not a lot to go on to pick up the fact that it has shared context. I’m wondering how this is handled when the rule is run, is it really “parsed back” from the // context: comment, or is there some other “hidden connection”.

It doesn’t feel good to use the comment as a way to detect “shared context” in DSL, so if there’s any other way to detect it, I would prefer that.

Edit: It looks like that is indeed the “connection”. It’s the comment that enables the script execution to look up and apply the shared context:

What’s worse is that this context comment is included whether the context contains anything or not. That is a big problem here, because that means that there’s no way to know which DSL rules can be safely exported to YAML.

I’ve managed to retrieve and include the “source” for DSL rules, but there’s a problem - the shared context is missing. There is just an empty line where the global variable should have been defined:

The source file is this:

var globalVariable = "Global variable"

rule "Test rule"
when
    Item MySwitch received command
then
    logInfo('test', "Global variable is " + globalVariable);
end

I was lucky. I took a chance and called .getParent() on the “rule node”, and then it includes everything:

However, I still need to figure out a way to determine if there is any shared context or not.

I assume the changes for converting .rules files to .yaml files are in the org.openhab.core.model.rule{,.ide,.runtime} bundles, so that moving DSL Script and DSL Rule from core to an add-on can still be performed.

One day the “DSL Script” syntax could be extended to support import declarations. The “DSL Rule” syntax could be extended to permit writing between rules (end … HERE … rule) anything, e.g. logError(), (early) return;, etc. The “DSL Rule” syntax could be extended, so that the rule … end returns an org.openhab.core.automation.Rule instance, which can be assigned to a variable. Other JSR223 languages allow “writing code between Rule definitions”, and I think in “DSL Rules” syntax this is not possible, just because nobody has implemented it.

These future developments should be taken into account here. One way to take this into account is to define an interface in core, which converts “automation input” to “yaml rule output”. If a JSR223 add-on implements the interface, then this add-on can convert its input into yaml rule output.

About class MIMETypeAliases I think it is suboptimal to define these aliases for automation add-ons in core. Each automation add-on should define its aliases. In fact from javax.script.ScriptEngineFactory getLanguageName(), getMIMETypes(), getNames() can be considered, when determining the list of aliases (for actions.config.type). This would mean e.g. that the Nashorn add-on is altered to report having the aliases NashornJS, ECMAScript5.1 and ECMAScript 5.1, if this is currently not the case.

Won’t that get you multiple rules if there are multiple defined in a file?

Thus far, it all takes place in org.openhab.core.model.rule.runtime.fileconverter.DslRuleFileConverter.

It is suboptimal, we agreed as much when we discussed this during development, but it’s nothing that prevents replacing it with a “real source” in the future, if that is made available. However, there are multiple problems with using ScriptEngineFactory as a source, one is that the information available there is inconsistent and/or incomplete (don’t remember the details), another is that during startup, this information is needed before the script engines are up and running (as far as I can remember). There exists some GitHub issue where I suggested trying to fix the whole “details registration” for the scripting languages, this is also an inconsistency between transformations and scripts/rules. But, nothing much has come of it (yet?).

Yes, but that is still “the source” for the rule. I don’t think we should attempt to find a way to include the globals and just the “correct rule”, because I think this would have a high likelihood of complications. It looks like “the global context” and each rule exists as different nodes in the model, so in theory, if a copy could be made of the whole “object”, one could delete the unrelated rules and just have the “global” and the “current rule” left. But, this is complicated, risky, potentially slow, and would make “the source” be an imaginary thing that doesn’t actually exist anywhere.

Also, it seems like there’s a relatively big problem here: It seems that in the model, all newlines are removed and stored separatelly simply as “indexes”. I suspect that this explains the extra blank lines when I didn’t call getParent(), and I assume that it means that we would still get blank lines where all “the other rules” would be.

It’s the same challenge with other scripting languages (except the newline stuff), if you want to show shared context, you’ll also get the source for other rules created by the same file.

It seems if there is any text returned by getParent() then there is at least the possibility of shared context. If you can’t rely on that, you’d have top do some regex grepping through the code to see if symbols defined in the parent appear in the text between then and end.

There is a slim possibility that getParent() only returns anything when the rule uses a variable from the global context. In that case you can use getParent(). You just need to compare what you get with these:

var globalVariable = "Global Variable"

rule "Global rule"
when
    Item MySwitch received command
then
    logInfo("test", "Global variable is " + globalVariable)
end

rule "Self contained rule"
when
    Item MySwitch received command
then
    logInfo("test", "Global variable is not in use")
end

There seems to always be a parent there, however, the shared context and other rules seem to exist as siblings, so I can check whether or not it has any siblings. But, I must be sure to “cover all the bases” here.

Can there be shared context between or after the rules? How does the import section look?

Not at this time.

Identical to what you have in a Java class file.

Ok, it turns out that imports and variables become different nodes. Imports become XImportSectionImpl (Eclipse class) instances, while variable declarations become VariableDeclarationImpl (OH class) instances. Each variable declaration becomes one node.

These are both siblings with the rules themselves, so I need to make the existence of both of these “disqualifying” for rule export.

This now correctly detects shared context for all my tests:

    private boolean hasSharedContext(INode ruleNode) {
        INode node = ruleNode;
        EObject eObject;
        while ((node = node.getPreviousSibling()) != null) {
            if (node instanceof ILeafNode leaf && leaf.isHidden()) {
                continue;
            }
            if ((eObject = node.getSemanticElement()) != null && !(eObject instanceof RuleImpl)) {
                return true;
            }
        }

        node = ruleNode;
        while ((node = node.getNextSibling()) != null) {
            if (node instanceof ILeafNode leaf && leaf.isHidden()) {
                continue;
            }
            if ((eObject = node.getSemanticElement()) != null && !(eObject instanceof RuleImpl)) {
                return true;
            }
        }

        return false;
    }

That includes this file with two rules but no shared context:


rule "Self contained rule 1"
when
    Item MySwitch received command
then
    logInfo("test", "Global variable is not")
end

rule "Self contained rule 2"
when
    Item MySwitch received command
then
    logInfo("test", "Global variable is not in use")
end

It turns out that blank lines where a “challenge” as well, as they end up as “hidden” nodes.

When it comes to converting to DSL, I think the point might have been reached to give up on that. Even though I haven’t properly solved how to create all kind of triggers, it works with some triggers, and this is the result:

rule "Initialize demo items" when System started then DemoLocation . postUpdate("52,9" ) DemoContact . postUpdate(OPEN ) DemoString . postUpdate("Hello SmartHome!" ) DemoDateTime . postUpdate( new DateTimeType( ) ) DemoNumber . postUpdate(12.34 ) end

..with this source:

rule "Initialize demo items"
when
  System started
then
  DemoLocation.postUpdate("52,9")
  DemoContact.postUpdate(OPEN)
  DemoString.postUpdate("Hello SmartHome!")
  DemoDateTime.postUpdate(new DateTimeType())
  DemoNumber.postUpdate(12.34)
end

So, while “technically working”, it has no newlines and some very strange space rules (both sides of ., and before ) it seems).

Given the somewhat marginal usefulness of exporting to DSL and all the challenges with doing it, I don’t think it’s worth it. It’s obvious that this has never been made with serialization in mind, and for me to go in and figure out how to control the formatting, just feels like a too big undertaking. I don’t even know if it’s possible, given that blank lines and newlines are stored the way they are, as “hidden elements” that are lost when its converted to a Rule, and that can’t really be restored when going back.

The consequence of this will also be that “DSL view” won’t be available in the “Code” tab, and that you can’t edit a managed DSL rule “as DSL” in the UI - but you can still edit the DSL action in YAML view.

Any thoughts about this? I just think there are too many problems/obstacles for too little value..?

My reading of Xtext - Language Implementation is that the parser merges sequence of whitespaces into one. Pretty-printing a model from memory to string should be done by a “formatter”.

I suggest having a Code tab, which includes all the text on a single line with a space after each token. This is better than no code tab.

Somebody later could write a formatter, (org.openhab.core.model.rule.formatting.RulesFormatter?) which does pretty-printing. Or adjust the system to keep the whitespaces during parsing.

It is possible to setup relatively easily what is generated. Here is how it is done for items and things. I guess it should be possible to do the same for tules to add carriage return at the right places.

It’s not whether to have a code tab or not - it’s whether the rule can be edited as DSL and YAML, or just YAML.

Thanks, I’ll have a look - but it should be said that whitespace in code is a bit “harder to deal” with than whitespace in “formal syntax”. What I mean by that is that making rules for the name, triggers etc. is comparable to that it Things and Items, but whitespace within the Action is not - and it might significantly alter what the user prefers.

There already is a rule formatter, it just does nothing:

class RulesFormatter extends AbstractDeclarativeFormatter {

//	@Inject extension RulesGrammarAccess
	
	override protected void configureFormatting(FormattingConfig c) {
// It's usually a good idea to activate the following three statements.
// They will add and preserve newlines around comments
//		c.setLinewrap(0, 1, 2).before(SL_COMMENTRule)
//		c.setLinewrap(0, 1, 2).before(ML_COMMENTRule)
//		c.setLinewrap(0, 1, 1).after(ML_COMMENTRule)
	}
}

The file to adjust is probably this one