Skip to content

Commit e147966

Browse files
committed
feat(emtp): add new plugin entity-task-mapper-plugin
1 parent 2658f27 commit e147966

File tree

6 files changed

+376
-0
lines changed

6 files changed

+376
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: 'Check Pull Request for plugin: entity-mapper-task-plugin'
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
paths:
7+
- 'emtp/**'
8+
9+
jobs:
10+
tests:
11+
name: Unit tests
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- name: Set up JDK 21 for x64
16+
uses: actions/setup-java@v4
17+
with:
18+
java-version: '21'
19+
distribution: 'temurin'
20+
architecture: x64
21+
22+
- name: Install Pandoc
23+
run: sudo apt-get update && sudo apt-get install -y pandoc
24+
25+
- name: Execute unit tests
26+
run: mvn -ntp -pl emtp test
27+
28+
- name: Execute mutation tests
29+
run: mvn -ntp -pl emtp org.pitest:pitest-maven:mutationCoverage
30+
31+
- name: Extract summary from pitest
32+
run: |
33+
echo "<html><head></head><body><h1>Pit Test Coverage Report: entity-mapper-task-plugin</h1><h3>Project Summary</h3>" > pitest.html
34+
perl -0777 -ne 'print "$1\n" if /(<table>.*?<\/table>)/s' emtp/target/pit-reports/index.html >> pitest.html
35+
echo "</body></html>" >> pitest.html
36+
37+
- name: Convert pitest report to markdown
38+
run: pandoc --from html --to markdown_github --no-highlight pitest.html
39+
40+
- name: Post comment
41+
uses: luukkemp/pr-comment@2024.1
42+
env:
43+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
with:
45+
path: pitest.html

emtp/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# 🌐 Entity Mapper Task Plugin (emtp)
2+
3+
The `emtp` module provides a task plugin for mapping values from the execution context into dynamic entities. It is part of the LinID task engine and allows flexible mapping configuration using Jinja templates.
4+
5+
## EntityMapperTaskPlugin
6+
7+
The `EntityMapperTaskPlugin` is a task plugin that populates entity attributes dynamically based on the execution context. It retrieves values defined in the task configuration and assigns them to corresponding fields in the dynamic entity.
8+
9+
### ✅ Use Case
10+
11+
Use this plugin when you need to:
12+
13+
* Map values from the execution context to entity attributes.
14+
* Dynamically compute entity fields using templates.
15+
* Simplify the integration of external data or precomputed values into entities.
16+
17+
### 🔧 Configuration
18+
19+
#### Minimal Example
20+
21+
```yaml
22+
task:
23+
- name: X
24+
type: entity-mapper
25+
mapping:
26+
id: '{{context.response.id}}'
27+
```
28+
29+
### Configuration Fields
30+
31+
| Key | Required | Description |
32+
| --------- | -------- |---------------------------------------------------|
33+
| `name` | ✅ | Name of the task |
34+
| `type` | ✅ | Must be `entity-mapper` |
35+
| `mapping` | ✅ | Entity fields mapping, supports Jinja templating. |
36+
37+
### 🛠 Behavior
38+
39+
* For each entry in `mapping`, the plugin renders the Jinja template using the `JinjaService` with the current execution context and entity.
40+
* The result is assigned to the corresponding attribute in the `DynamicEntity`.
41+
* If the `mapping` option is missing, the plugin throws an `ApiException` with a descriptive error message.
42+
43+
### Example Mapping
44+
45+
```yaml
46+
task:
47+
- name: MapResponseId
48+
type: entity-mapper
49+
mapping:
50+
id: '{{context.response.id}}'
51+
name: '{{context.response.name}}'
52+
status: '{{context.response.status}}'
53+
```
54+
55+
In this example, the entity's `id`, `name`, and `status` attributes will be populated dynamically based on the values in `context.response`.
56+
57+
### 🔑 Notes
58+
59+
* Templates support any values available in the `TaskExecutionContext` and the entity itself.
60+
* This plugin is purely a **task plugin**; it does not call external APIs or transform responses—use in combination with other plugins (like HTTP or JSON parsing) if needed.
61+
* Missing mappings or invalid templates will result in a runtime exception.

emtp/pom.xml

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
<modelVersion>4.0.0</modelVersion>
4+
<parent>
5+
<groupId>io.github.linagora.linid.im</groupId>
6+
<artifactId>linid-im-api-community-plugins</artifactId>
7+
<version>0.1.0</version>
8+
</parent>
9+
10+
<groupId>io.github.linagora.linid.im</groupId>
11+
<artifactId>emtp</artifactId>
12+
<packaging>jar</packaging>
13+
14+
<version>0.1.0</version>
15+
<name>entity-mapper-task-plugin</name>
16+
<description>Plugin for mapping values from the execution context into an entity, allowing flexible configuration to extract and assign data to corresponding entity fields.</description>
17+
<url>https://github.com/linagora/linid-im-api-community-plugins</url>
18+
19+
<licenses>
20+
<license>
21+
<name>GNU Affero General Public License v3.0</name>
22+
<url>https://www.gnu.org/licenses/agpl-3.0.html</url>
23+
<distribution>repo</distribution>
24+
<comments>This project is licensed under the GNU AGPL v3.0</comments>
25+
</license>
26+
</licenses>
27+
28+
<developers>
29+
<developer>
30+
<id>Zorin95670</id>
31+
<name>Vincent MOITTIE</name>
32+
<email>moittie.vincent@gmail.com</email>
33+
</developer>
34+
</developers>
35+
36+
<scm>
37+
<connection>scm:git:git://github.com/linagora/linid-im-api-community-plugins.git</connection>
38+
<developerConnection>scm:git:ssh://git@github.com:linagora/linid-im-api-community-plugins.git</developerConnection>
39+
<url>https://github.com/linagora/linid-im-api-community-plugins</url>
40+
</scm>
41+
<properties>
42+
<sonar.projectBaseDir>emtp</sonar.projectBaseDir>
43+
</properties>
44+
45+
<dependencies>
46+
<dependency>
47+
<groupId>com.hubspot.jinjava</groupId>
48+
<artifactId>jinjava</artifactId>
49+
<version>2.8.3</version>
50+
<scope>test</scope>
51+
</dependency>
52+
</dependencies>
53+
54+
<build>
55+
<plugins>
56+
<plugin>
57+
<artifactId>maven-shade-plugin</artifactId>
58+
<version>3.6.2</version>
59+
<executions>
60+
<execution>
61+
<phase>package</phase>
62+
<goals>
63+
<goal>shade</goal>
64+
</goals>
65+
<configuration>
66+
<shadedArtifactAttached>false</shadedArtifactAttached>
67+
<createDependencyReducedPom>false</createDependencyReducedPom>
68+
</configuration>
69+
</execution>
70+
</executions>
71+
</plugin>
72+
</plugins>
73+
</build>
74+
</project>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package io.github.linagora.linid.im.emtp;
2+
3+
import io.github.linagora.linid.im.corelib.exception.ApiException;
4+
import io.github.linagora.linid.im.corelib.i18n.I18nMessage;
5+
import io.github.linagora.linid.im.corelib.plugin.config.JinjaService;
6+
import io.github.linagora.linid.im.corelib.plugin.config.dto.TaskConfiguration;
7+
import io.github.linagora.linid.im.corelib.plugin.entity.DynamicEntity;
8+
import io.github.linagora.linid.im.corelib.plugin.task.TaskExecutionContext;
9+
import io.github.linagora.linid.im.corelib.plugin.task.TaskPlugin;
10+
import lombok.NonNull;
11+
import org.springframework.stereotype.Component;
12+
import tools.jackson.core.type.TypeReference;
13+
14+
import java.util.Map;
15+
16+
/**
17+
* Task plugin that maps values from the execution context into a dynamic entity.
18+
*
19+
* <p>This plugin uses templates defined in the task configuration to populate entity attributes dynamically.
20+
* It supports tasks of type "entity-mapping".
21+
*/
22+
@Component
23+
public class EntityMappingTaskPlugin implements TaskPlugin {
24+
25+
/**
26+
* Service used to render Jinja templates with the task execution context and entity attributes.
27+
*/
28+
private final JinjaService jinjaService;
29+
30+
/**
31+
* Constructs an EntityMappingTaskPlugin with the specified JinjaService.
32+
*
33+
* @param jinjaService the service responsible for rendering Jinja templates
34+
*/
35+
public EntityMappingTaskPlugin(final JinjaService jinjaService) {
36+
this.jinjaService = jinjaService;
37+
}
38+
39+
@Override
40+
public boolean supports(final @NonNull String type) {
41+
return "entity-mapping".equalsIgnoreCase(type);
42+
}
43+
44+
@Override
45+
public void execute(TaskConfiguration taskConfiguration, DynamicEntity dynamicEntity, TaskExecutionContext taskExecutionContext) {
46+
Map<String, String> mapping = taskConfiguration.getOption(
47+
"mapping",
48+
new TypeReference<Map<String, String>>() {}
49+
)
50+
.orElseThrow(() -> new ApiException(
51+
500,
52+
I18nMessage.of("error.plugin.default.missing.option", Map.of("option", "mapping"))
53+
));
54+
55+
mapping.forEach((String key, String template) -> {
56+
String value = jinjaService.render(taskExecutionContext, dynamicEntity, template);
57+
dynamicEntity.getAttributes().put(key, value);
58+
});
59+
}
60+
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package io.github.linagora.linid.im.emtp;
2+
3+
import io.github.linagora.linid.im.corelib.exception.ApiException;
4+
import io.github.linagora.linid.im.corelib.plugin.config.JinjaService;
5+
import io.github.linagora.linid.im.corelib.plugin.config.dto.TaskConfiguration;
6+
import io.github.linagora.linid.im.corelib.plugin.entity.DynamicEntity;
7+
import io.github.linagora.linid.im.corelib.plugin.task.TaskExecutionContext;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
import static org.junit.jupiter.api.Assertions.assertEquals;
15+
import static org.junit.jupiter.api.Assertions.assertFalse;
16+
import static org.junit.jupiter.api.Assertions.assertThrows;
17+
import static org.junit.jupiter.api.Assertions.assertTrue;
18+
19+
@DisplayName("Test class: EntityMappingTaskPlugin")
20+
class EntityMappingTaskPluginTest {
21+
22+
@Test
23+
@DisplayName("test supports: should return true on valid type")
24+
void testSupports() {
25+
var plugin = new EntityMappingTaskPlugin(null);
26+
27+
assertTrue(plugin.supports("entity-mapping"));
28+
assertFalse(plugin.supports("other"));
29+
}
30+
31+
@Test
32+
@DisplayName("test execute: should throw error without mapping configuration")
33+
void testExecuteThrowError() {
34+
var plugin = new EntityMappingTaskPlugin(null);
35+
var configuration = new TaskConfiguration();
36+
var context = new TaskExecutionContext();
37+
38+
ApiException exception = assertThrows(ApiException.class, () -> plugin.execute(configuration, new DynamicEntity(), context));
39+
40+
assertEquals("error.plugin.default.missing.option", exception.getMessage());
41+
assertEquals("mapping", exception.getError().context().get("option"));
42+
}
43+
44+
@Test
45+
@DisplayName("test execute: should do nothing with empty mapping configuration")
46+
void testExecuteDoNothing() {
47+
var plugin = new EntityMappingTaskPlugin(null);
48+
var configuration = new TaskConfiguration();
49+
configuration.addOption("mapping", Map.of());
50+
var context = new TaskExecutionContext();
51+
var entity = new DynamicEntity();
52+
entity.setAttributes(new HashMap<>());
53+
entity.getAttributes().put("name", "test");
54+
55+
plugin.execute(configuration, new DynamicEntity(), context);
56+
57+
assertEquals(1, entity.getAttributes().size());
58+
assertEquals("test", entity.getAttributes().get("name"));
59+
}
60+
61+
@Test
62+
@DisplayName("test execute: should put new value into entity")
63+
void testExecutePutNewValue() {
64+
var plugin = new EntityMappingTaskPlugin(new JinjaServiceTest());
65+
var configuration = new TaskConfiguration();
66+
configuration.addOption("mapping", Map.of("id", "value"));
67+
var context = new TaskExecutionContext();
68+
var entity = new DynamicEntity();
69+
entity.setAttributes(new HashMap<>());
70+
entity.getAttributes().put("name", "test");
71+
72+
plugin.execute(configuration, entity, context);
73+
74+
assertEquals(2, entity.getAttributes().size());
75+
assertEquals("test", entity.getAttributes().get("name"));
76+
assertEquals("value", entity.getAttributes().get("id"));
77+
}
78+
79+
@Test
80+
@DisplayName("test execute: should update new value into entity")
81+
void testExecuteUpdateNewValue() {
82+
var plugin = new EntityMappingTaskPlugin(new JinjaServiceTest());
83+
var configuration = new TaskConfiguration();
84+
configuration.addOption("mapping", Map.of("name", "value"));
85+
var context = new TaskExecutionContext();
86+
var entity = new DynamicEntity();
87+
entity.setAttributes(new HashMap<>());
88+
entity.getAttributes().put("name", "test");
89+
90+
plugin.execute(configuration, entity, context);
91+
92+
assertEquals(1, entity.getAttributes().size());
93+
assertEquals("value", entity.getAttributes().get("name"));
94+
}
95+
96+
@Test
97+
@DisplayName("test execute: should update and put multiple values into entity")
98+
void testExecutePutUpdateMultipleValues() {
99+
var plugin = new EntityMappingTaskPlugin(new JinjaServiceTest());
100+
var configuration = new TaskConfiguration();
101+
configuration.addOption("mapping", Map.of(
102+
"name", "value",
103+
"id", "test2"
104+
));
105+
var context = new TaskExecutionContext();
106+
var entity = new DynamicEntity();
107+
entity.setAttributes(new HashMap<>());
108+
entity.getAttributes().put("name", "test");
109+
110+
plugin.execute(configuration, entity, context);
111+
112+
assertEquals(2, entity.getAttributes().size());
113+
assertEquals("value", entity.getAttributes().get("name"));
114+
assertEquals("test2", entity.getAttributes().get("id"));
115+
}
116+
117+
class JinjaServiceTest implements JinjaService {
118+
119+
@Override
120+
public String render(TaskExecutionContext taskExecutionContext, String s) {
121+
return s;
122+
}
123+
124+
@Override
125+
public String render(TaskExecutionContext taskExecutionContext, DynamicEntity dynamicEntity, String s) {
126+
return s;
127+
}
128+
129+
@Override
130+
public String render(TaskExecutionContext taskExecutionContext, DynamicEntity dynamicEntity, Map<String, Object> map, String s) {
131+
return s;
132+
}
133+
}
134+
}

pom.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<module>lvp</module>
1616
<module>jptp</module>
1717
<module>dlvp</module>
18+
<module>emtp</module>
19+
<module>emtp</module>
1820
</modules>
1921
<licenses>
2022
<license>

0 commit comments

Comments
 (0)