Repository: kestra-io/kestra (java)

Issue:
Provide a `| string` filter
### Feature description

Like it exists a `| number` Pebble filter that converts from a number in string representation to a number (INT). The reverse does not exist: converting an INT variable into a STRING.

This request is about creating the Pebble filter: `| string` which converts from number to string.

Pull request description:
feat:string filter
<!-- Thanks for submitting a Pull Request to Kestra. To help us review your contribution, please follow the guidelines below:

- Make sure that your commits follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification e.g. `feat(ui): add a new navigation menu item` or `fix(core): fix a bug in the core model` or `docs: update the README.md`. This will help us automatically generate the changelog.
- The title should briefly summarize the proposed changes.
- Provide a short overview of the change and the value it adds.
- Share a flow example to help the reviewer understand and QA the change.
- Use "closes" to automatically close an issue. For example, `closes #1234` will close issue #1234. -->

### What changes are being made and why?

<!-- Please include a brief summary of the changes included in this PR e.g. closes #1234. -->

---
added a filter for converting numbers and objects to string closes #5528 
### How the changes have been QAed?

<!-- Include example code that shows how this PR has been QAed. The code should present a complete yet easily reproducible flow.

```yaml
# Your example flow code here
```

Note that this is not a replacement for unit tests but rather a way to demonstrate how the changes work in a real-life scenario, as the end-user would experience them.

Remove this section if this change applies to all flows or to the documentation only. -->

---
Example flow code demonstrating the use of the string filter in a Pebble template

template: |

 ```yaml
  {{ 123 | string }}   # Expected output: "123"
  {{ "456.78" | string }}  # Expected output: "456.78"
 ```

<!--If there are any setup requirements like API keys or trial accounts, kindly include brief bullet-points-description outlining the setup process below.

- [External System Documentation](URL)
- Steps to set up the necessary resources

If there are no setup requirements, you can remove this section.

Thank you for your contribution. ❤️  -->


Patch:
diff --git a/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java b/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java
index 580cb586f..63e0c1dff 100644
--- a/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java
+++ b/core/src/main/java/io/kestra/core/plugins/DefaultPluginRegistry.java
@@ -217,7 +217,7 @@ public class DefaultPluginRegistry implements PluginRegistry {
             if (identifier == null || identifier.isBlank()) {
                 throw new IllegalArgumentException("Cannot create plugin identifier from null or empty string");
             }
-            return new ClassTypeIdentifier(identifier.toLowerCase(Locale.ROOT));
+            return new ClassTypeIdentifier(identifier);
         }
 
         /**
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/Extension.java b/core/src/main/java/io/kestra/core/runners/pebble/Extension.java
index 4dce19175..0be75fed1 100644
--- a/core/src/main/java/io/kestra/core/runners/pebble/Extension.java
+++ b/core/src/main/java/io/kestra/core/runners/pebble/Extension.java
@@ -113,6 +113,7 @@ public class Extension extends AbstractExtension {
         filters.put("sha1", new Sha1Filter());
         filters.put("sha512", new Sha512Filter());
         filters.put("md5", new Md5Filter());
+        filters.put("string", new StringFilter());
         return filters;
     }
 
diff --git a/core/src/main/java/io/kestra/core/runners/pebble/filters/StringFilter.java b/core/src/main/java/io/kestra/core/runners/pebble/filters/StringFilter.java
new file mode 100644
index 000000000..a4e53819e
--- /dev/null
+++ b/core/src/main/java/io/kestra/core/runners/pebble/filters/StringFilter.java
@@ -0,0 +1,25 @@
+package io.kestra.core.runners.pebble.filters;
+
+import io.pebbletemplates.pebble.error.PebbleException;
+import io.pebbletemplates.pebble.template.EvaluationContext;
+import io.pebbletemplates.pebble.template.PebbleTemplate;
+import io.pebbletemplates.pebble.extension.Filter;
+import java.util.List;
+import java.util.Map;
+
+public class StringFilter implements Filter {
+
+    @Override
+    public Object apply(Object input, Map<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) throws PebbleException {
+        if (input == null) return null;
+
+        if (input instanceof String) return input;
+
+        return input.toString();
+    }
+
+    @Override
+    public List<String> getArgumentNames() {
+        return null;
+    }
+}
diff --git a/ui/src/assets/docs/basic.md b/ui/src/assets/docs/basic.md
index 3587d9eb6..ef8266942 100644
--- a/ui/src/assets/docs/basic.md
+++ b/ui/src/assets/docs/basic.md
@@ -322,6 +322,7 @@ The table below lists Pebble functions and filter expressions:
 | `fileSize`       | `{{ fileSize(output.download.uri) }}` — Returns the size of the file present at the given uri location.                         |
 | `fileExists`     | `{{ fileExists(output.download.uri) }}` — Returns true if file is present at the given uri location.                            |
 | `isFileEmpty`    | `{{ isFileEmpty(output.download.uri) }}` — Returns true if file present at the given uri location is empty.                       |
+| `string`         | `{{ 123 \| string }}` — Converts 123 into a string.                                                                       |
 
 
 


Test patch:
diff --git a/core/src/test/java/io/kestra/core/plugins/ClassTypeIdentifierTest.java b/core/src/test/java/io/kestra/core/plugins/ClassTypeIdentifierTest.java
new file mode 100644
index 000000000..312c5700e
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/plugins/ClassTypeIdentifierTest.java
@@ -0,0 +1,17 @@
+package io.kestra.core.plugins;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+public class ClassTypeIdentifierTest {
+    @Test
+    void caseMatters() {
+        String identifier = "io.kestra.core.plugins.serdes.PluginDeserializerTest.TestPlugin";
+        assertThat(
+            DefaultPluginRegistry.ClassTypeIdentifier.create(identifier).type(),
+            is(identifier)
+        );
+    }
+}
diff --git a/core/src/test/java/io/kestra/core/runners/pebble/filters/StringFilterTest.java b/core/src/test/java/io/kestra/core/runners/pebble/filters/StringFilterTest.java
new file mode 100644
index 000000000..7ef0b56d8
--- /dev/null
+++ b/core/src/test/java/io/kestra/core/runners/pebble/filters/StringFilterTest.java
@@ -0,0 +1,35 @@
+package io.kestra.core.runners.pebble.filters;
+
+import io.kestra.core.exceptions.IllegalVariableEvaluationException;
+import io.kestra.core.junit.annotations.KestraTest;
+import io.kestra.core.runners.VariableRenderer;
+import jakarta.inject.Inject;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@KestraTest
+public class StringFilterTest {
+
+    @Inject
+    VariableRenderer variableRenderer;
+
+    static Stream<Arguments> source() {
+        return Stream.of(
+            Arguments.of("{{ 12.3 | string | className }}", String.class.getName()),
+            Arguments.of("{{ {\"field\":\"hello\"} | string | className }}", String.class.getName())
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource("source")
+    void run(String exp, String expected) throws IllegalVariableEvaluationException {
+        String render = variableRenderer.render(exp, Map.of());
+        assertThat(render, is(expected));
+    }
+}