šŸšø Allow specifying ā€œEmailActionā€ to use a single email action for everything
authorDavid ā€˜Bombeā€™ Roden <bombe@pterodactylus.net>
Wed, 13 Nov 2024 21:43:03 +0000 (22:43 +0100)
committerDavid ā€˜Bombeā€™ Roden <bombe@pterodactylus.net>
Wed, 13 Nov 2024 21:43:03 +0000 (22:43 +0100)
12 files changed:
src/main/java/net/pterodactylus/rhynodge/engine/Engine.java
src/main/java/net/pterodactylus/rhynodge/loader/ReactionLoader.java
src/test/kotlin/net/pterodactylus/rhynodge/loader/ReactionLoaderTest.kt
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-action-parameters.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-email-action.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-filter-parameters.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-merger-parameters.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-query-parameters.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-watcher-parameters.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-watcher.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/chain-without-watcher.json [new file with mode: 0644]
src/test/resources/net/pterodactylus/rhynodge/loader/disabled-chain.json [new file with mode: 0644]

index e132b08..963997b 100644 (file)
 package net.pterodactylus.rhynodge.engine;
 
 import jakarta.inject.Inject;
+import jakarta.inject.Named;
 import jakarta.inject.Singleton;
 import kotlin.Unit;
 import kotlin.jvm.functions.Function0;
 import kotlin.jvm.functions.Function1;
+import net.pterodactylus.rhynodge.Action;
 import net.pterodactylus.rhynodge.Reaction;
 import net.pterodactylus.rhynodge.actions.EmailAction;
 import net.pterodactylus.rhynodge.states.StateManager;
@@ -46,10 +48,10 @@ public class Engine {
        private final StateManager stateManager;
        private final ScheduledExecutorService executorService;
        private final Map<String, Future<?>> scheduledFutures = new ConcurrentHashMap<>();
-       private final EmailAction errorEmailAction;
+       private final Action errorEmailAction;
 
        @Inject
-       public Engine(StateManager stateManager, EmailAction errorEmailAction) {
+       public Engine(StateManager stateManager, @Named("errorEmail") Action errorEmailAction) {
                this.stateManager = stateManager;
                this.errorEmailAction = errorEmailAction;
                executorService = new ScheduledThreadPoolExecutor(1);
index d358c23..44fa217 100644 (file)
@@ -42,10 +42,8 @@ import net.pterodactylus.rhynodge.Merger;
 public class ReactionLoader {
 
        @Inject
-       public ReactionLoader(@Named("smtpHostname") String smtpHostname, @Named("emailSender") String emailSender, @Named("emailRecipient") String emailRecipient) {
-               this.smtpHostname = smtpHostname;
-               this.emailSender = emailSender;
-               this.emailRecipient = emailRecipient;
+       public ReactionLoader(@Named("email") Action emailAction) {
+               this.emailAction = emailAction;
        }
 
        /**
@@ -146,6 +144,10 @@ public class ReactionLoader {
        @SuppressWarnings("unchecked")
        private <T> T createObject(String className, String packageName, List<String> parameters) throws LoaderException {
 
+               if (className.equals("EmailAction")) {
+                       return (T) emailAction;
+               }
+
                /* try to load class without package name. */
                Class<?> objectClass = null;
                try {
@@ -163,13 +165,11 @@ public class ReactionLoader {
                        }
                }
 
-               var effectiveParameters = overrideParameters(className, parameters);
-
                /* locate an eligible constructor. */
                Constructor<?> wantedConstructor = null;
                for (Constructor<?> constructor : objectClass.getConstructors()) {
                        Class<?>[] parameterTypes = constructor.getParameterTypes();
-                       if (parameterTypes.length != effectiveParameters.size()) {
+                       if (parameterTypes.length != parameters.size()) {
                                continue;
                        }
                        boolean compatibleTypes = true;
@@ -190,7 +190,7 @@ public class ReactionLoader {
                }
 
                try {
-                       return (T) wantedConstructor.newInstance(effectiveParameters.toArray());
+                       return (T) wantedConstructor.newInstance(parameters.toArray());
                } catch (IllegalArgumentException iae1) {
                        throw new LoaderException("Could not invoke constructor.", iae1);
                } catch (InstantiationException ie1) {
@@ -203,15 +203,6 @@ public class ReactionLoader {
 
        }
 
-       private List<String> overrideParameters(String className, List<String> parameters) {
-               if (className.equals("EmailAction")) {
-                       return List.of(smtpHostname, emailSender, emailRecipient);
-               }
-               return parameters;
-       }
-
-       private final String smtpHostname;
-       private final String emailSender;
-       private final String emailRecipient;
+       private final Action emailAction;
 
 }
index c20a362..e177d09 100644 (file)
 package net.pterodactylus.rhynodge.loader
 
-import com.google.inject.Guice
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.readValue
+import com.google.inject.Guice.createInjector
+import net.pterodactylus.rhynodge.Action
+import net.pterodactylus.rhynodge.Filter
+import net.pterodactylus.rhynodge.Merger
+import net.pterodactylus.rhynodge.Query
+import net.pterodactylus.rhynodge.State
+import net.pterodactylus.rhynodge.Watcher
+import net.pterodactylus.rhynodge.actions.EmailAction
+import net.pterodactylus.rhynodge.output.Output
+import net.pterodactylus.rhynodge.states.AbstractState
 import net.pterodactylus.util.inject.ObjectBinding
+import org.hamcrest.Matcher
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.contains
+import org.hamcrest.Matchers.equalTo
+import org.hamcrest.Matchers.instanceOf
+import org.hamcrest.Matchers.sameInstance
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertThrows
+import java.util.concurrent.TimeUnit
 
 class ReactionLoaderTest {
 
        @Test
        fun `reaction loader can be created by guice`() {
-               val injector = Guice.createInjector(
-                       ObjectBinding.forClass(String::class.java).named("smtpHostname").`is`("host"),
-                       ObjectBinding.forClass(String::class.java).named("emailSender").`is`("sender"),
-                       ObjectBinding.forClass(String::class.java).named("emailRecipient").`is`("recipient"),
+               val injector = createInjector(
+                       ObjectBinding.forClass(Action::class.java).named("email").shallBe(emailAction)
                )
                injector.getInstance(ReactionLoader::class.java)
        }
 
+       @Test
+       fun `loader refuses to load disabled chains`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("disabled-chain.json")!!)
+               assertThrows<IllegalArgumentException> { reactionLoader.loadReaction(chain) }
+       }
+
+       @Test
+       fun `loader can load chain without watcher`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-without-watcher.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat(reaction.name(), equalTo("Test Reaction"))
+               assertThat(reaction.updateInterval(), equalTo(TimeUnit.SECONDS.toMillis(3600)))
+               assertThat(reaction.query(), instanceOf<TestQuery0>())
+               assertThat(reaction.filters(), contains(instanceOf<TestFilter0>()))
+               assertThat(reaction.merger(), instanceOf<TestMerger0>())
+               assertThat(reaction.action(), instanceOf<TestAction0>())
+       }
+
+       @Test
+       fun `loader can load chain with watcher`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-watcher.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat(reaction.name(), equalTo("Test Watcher"))
+               assertThat(reaction.updateInterval(), equalTo(TimeUnit.SECONDS.toMillis(7200)))
+               assertThat(reaction.query(), instanceOf<TestQuery0>())
+               assertThat(reaction.filters(), contains(instanceOf<TestFilter0>()))
+               assertThat(reaction.merger(), instanceOf<TestMerger0>())
+               assertThat(reaction.action(), instanceOf<TestAction0>())
+       }
+
+       @Test
+       fun `loader can instantiate query with multiple string parameters`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-query-parameters.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat((reaction.query() as TestQuery2).parameter1, equalTo("value1"))
+               assertThat((reaction.query() as TestQuery2).parameter2, equalTo("value2"))
+       }
+
+       @Test
+       fun `loader can instantiate filters with multiple string parameters`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-filter-parameters.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat(reaction.filters(), contains(instanceOf<TestFilter1>(), instanceOf<TestFilter2>()))
+               assertThat((reaction.filters().toList()[0] as TestFilter1).parameter, equalTo("value"))
+               assertThat((reaction.filters().toList()[1] as TestFilter2).first, equalTo("one"))
+               assertThat((reaction.filters().toList()[1] as TestFilter2).second, equalTo("two"))
+       }
+
+       @Test
+       fun `loader can instantiate merger with multiple parameters`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-merger-parameters.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat((reaction.merger() as TestMerger3).one, equalTo("1"))
+               assertThat((reaction.merger() as TestMerger3).two, equalTo("2"))
+               assertThat((reaction.merger() as TestMerger3).three, equalTo("3"))
+       }
+
+       @Test
+       fun `loader can instantiate action with multiple parameters`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-action-parameters.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat((reaction.action() as TestAction2).foo, equalTo("1"))
+               assertThat((reaction.action() as TestAction2).bar, equalTo("2"))
+       }
+
+       @Test
+       fun `loader can instantiate watcher with multiple parameters`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-watcher-parameters.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat((reaction.query() as TestQuery1).parameter1, equalTo("query"))
+               assertThat((reaction.filters().single() as TestFilter1).parameter, equalTo("filter"))
+               assertThat((reaction.merger() as TestMerger1).one, equalTo("merger"))
+       }
+
+       @Test
+       fun `loader replaces email actions with instance given in constructor`() {
+               val chain = objectMapper.readValue<Chain>(javaClass.getResourceAsStream("chain-with-email-action.json")!!)
+               val reaction = reactionLoader.loadReaction(chain)
+               assertThat(reaction.action(), sameInstance(emailAction))
+       }
+
+       private val emailAction = EmailAction("local.host", "send@r", "recipi@nt")
+       private val reactionLoader = ReactionLoader(emailAction)
+
+}
+
+private inline fun <reified T> instanceOf(): Matcher<Any> = instanceOf(T::class.java)
+
+open class TestQuery0 : Query {
+       override fun state(): State = object : AbstractState() {
+               override fun plainText() = ""
+       }
+}
+
+class TestQuery1(val parameter1: String) : TestQuery0()
+class TestQuery2(val parameter1: String, val parameter2: String) : TestQuery0()
+
+open class TestFilter0 : Filter {
+       override fun filter(state: State) = state
 }
+
+class TestFilter1(val parameter: String): TestFilter0()
+class TestFilter2(val first: String, val second: String): TestFilter0()
+
+open class TestMerger0 : Merger {
+       override fun mergeStates(previousState: State, currentState: State) = currentState.apply { trigger() }
+}
+class TestMerger1(val one: String) : TestMerger0()
+class TestMerger3(val one: String, val two: String, val three: String) : TestMerger0()
+
+open class TestAction0 : Action {
+       override fun execute(output: Output) = Unit
+}
+
+class TestAction2(val foo: String, val bar: String) : TestAction0()
+
+class TestWatcher0 : Watcher {
+       override fun query() = TestQuery0()
+       override fun filters() = listOf(TestFilter0())
+       override fun merger() = TestMerger0()
+}
+
+class TestWatcher3(val one: String, val two: String, val three: String) : Watcher {
+       override fun query() = TestQuery1(one)
+       override fun filters() = listOf(TestFilter1(two))
+       override fun merger() = TestMerger1(three)
+}
+
+private val objectMapper = ObjectMapper()
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-action-parameters.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-action-parameters.json
new file mode 100644 (file)
index 0000000..d9cc940
--- /dev/null
@@ -0,0 +1,29 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "query": {
+    "class": "net.pterodactylus.rhynodge.loader.TestQuery0"
+  },
+  "filters": [
+    {
+      "class": "net.pterodactylus.rhynodge.loader.TestFilter0"
+    }
+  ],
+  "merger": {
+    "class": "net.pterodactylus.rhynodge.loader.TestMerger0"
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction2",
+    "parameters": [
+      {
+        "name": "one",
+        "value": "1"
+      },
+      {
+        "name": "two",
+        "value": "2"
+      }
+    ]
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-email-action.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-email-action.json
new file mode 100644 (file)
index 0000000..dc27653
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "watcher": {
+    "class": "net.pterodactylus.rhynodge.loader.TestWatcher0"
+  },
+  "action": {
+    "class": "EmailAction"
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-filter-parameters.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-filter-parameters.json
new file mode 100644 (file)
index 0000000..4a92a12
--- /dev/null
@@ -0,0 +1,38 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "query": {
+    "class": "net.pterodactylus.rhynodge.loader.TestQuery0"
+  },
+  "filters": [
+    {
+      "class": "net.pterodactylus.rhynodge.loader.TestFilter1",
+      "parameters": [
+        {
+          "name": "parameter",
+          "value": "value"
+        }
+      ]
+    },
+    {
+    "class": "net.pterodactylus.rhynodge.loader.TestFilter2",
+      "parameters": [
+        {
+          "name": "first",
+          "value": "one"
+        },
+        {
+          "name": "second",
+          "value": "two"
+        }
+      ]
+    }
+  ],
+  "merger": {
+    "class": "net.pterodactylus.rhynodge.loader.TestMerger0"
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction0"
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-merger-parameters.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-merger-parameters.json
new file mode 100644 (file)
index 0000000..da68d00
--- /dev/null
@@ -0,0 +1,33 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "query": {
+    "class": "net.pterodactylus.rhynodge.loader.TestQuery0"
+  },
+  "filters": [
+    {
+      "class": "net.pterodactylus.rhynodge.loader.TestFilter0"
+    }
+  ],
+  "merger": {
+    "class": "net.pterodactylus.rhynodge.loader.TestMerger3",
+    "parameters": [
+      {
+        "name": "one",
+        "value": "1"
+      },
+      {
+        "name": "two",
+        "value": "2"
+      },
+      {
+        "name": "param2",
+        "value": "3"
+      }
+    ]
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction0"
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-query-parameters.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-query-parameters.json
new file mode 100644 (file)
index 0000000..10cd121
--- /dev/null
@@ -0,0 +1,29 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "query": {
+    "class": "net.pterodactylus.rhynodge.loader.TestQuery2",
+    "parameters": [
+      {
+        "name": "param1",
+        "value": "value1"
+      },
+      {
+        "name": "param2",
+        "value": "value2"
+      }
+    ]
+  },
+  "filters": [
+    {
+      "class": "net.pterodactylus.rhynodge.loader.TestFilter0"
+    }
+  ],
+  "merger": {
+    "class": "net.pterodactylus.rhynodge.loader.TestMerger0"
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction0"
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-watcher-parameters.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-watcher-parameters.json
new file mode 100644 (file)
index 0000000..8b138ea
--- /dev/null
@@ -0,0 +1,25 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "watcher": {
+    "class": "net.pterodactylus.rhynodge.loader.TestWatcher3",
+    "parameters": [
+      {
+        "name": "1",
+        "value": "query"
+      },
+      {
+        "name": "2",
+        "value": "filter"
+      },
+      {
+        "name": "3",
+        "value": "merger"
+      }
+    ]
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction0"
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-watcher.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-with-watcher.json
new file mode 100644 (file)
index 0000000..51444a0
--- /dev/null
@@ -0,0 +1,11 @@
+{
+  "enabled": true,
+  "name": "Test Watcher",
+  "watcher": {
+    "class": "net.pterodactylus.rhynodge.loader.TestWatcher0"
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction0"
+  },
+  "updateInterval": 7200
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/chain-without-watcher.json b/src/test/resources/net/pterodactylus/rhynodge/loader/chain-without-watcher.json
new file mode 100644 (file)
index 0000000..9788b16
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "enabled": true,
+  "name": "Test Reaction",
+  "query": {
+    "class": "net.pterodactylus.rhynodge.loader.TestQuery0"
+  },
+  "filters": [
+    {
+      "class": "net.pterodactylus.rhynodge.loader.TestFilter0"
+    }
+  ],
+  "merger": {
+    "class": "net.pterodactylus.rhynodge.loader.TestMerger0"
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction0"
+  },
+  "updateInterval": 3600
+}
diff --git a/src/test/resources/net/pterodactylus/rhynodge/loader/disabled-chain.json b/src/test/resources/net/pterodactylus/rhynodge/loader/disabled-chain.json
new file mode 100644 (file)
index 0000000..bf6928a
--- /dev/null
@@ -0,0 +1,17 @@
+{
+  "enabled": false,
+  "name": "Test Watcher",
+  "watcher": {
+    "class": "net.pterodactylus.rhynodge.loader.TestWatcher"
+  },
+  "action": {
+    "class": "net.pterodactylus.rhynodge.loader.TestAction",
+    "parameters": [
+      {
+        "name": "parameter",
+        "value": "value"
+      }
+    ]
+  },
+  "updateInterval": 7200
+}