Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7fa2a4c
feat(mapping): implement entity dynamic mapping dry-run workflow for …
foukou19 Jul 21, 2026
79c6f34
Merge branch 'main' into feat/idp-core-entity-dynamic-mapping-dry-run
foukou19 Jul 21, 2026
844a44b
feat(entity-dynamic-mapping): update entity relation
foukou19 Jul 24, 2026
f741d07
feat(entity-dynamic-mapping): update entity relation
foukou19 Jul 24, 2026
e766205
feat(entity-dynamic-mapping): update entity relation
foukou19 Jul 24, 2026
fc62a02
feat(entity-dynamic-mapping): update swagger.yaml
foukou19 Jul 27, 2026
ffa6350
feat(core): init camel ingestion route
brandPittCode Jul 27, 2026
ca258db
feat(core): init camel ingestion route
brandPittCode Aug 4, 2026
e8ea98b
feat(core): add ingestion mode to mapping and ingestion processor
brandPittCode Aug 5, 2026
b1a6d31
feat(core): add ingestion mode to mapping and ingestion processor
brandPittCode Aug 5, 2026
cfc4d0e
Merge branch 'main' into feat/add-http-ingestion-route
brandPittCode Aug 5, 2026
cc0f30e
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 Aug 6, 2026
46a174d
feat(core): improve coverage
foukou19 Aug 6, 2026
24efb34
feat(core): improve coverage
foukou19 Aug 6, 2026
4bc03ca
feat(core): remove webhook tracing implementation
foukou19 Aug 7, 2026
2eec39f
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 Aug 7, 2026
8be5834
feat(core): improve coverage
foukou19 Aug 10, 2026
1bcb3b2
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 Aug 10, 2026
50d6f17
feat(core): create dedicated spring security filter chain for webhooks
foukou19 Aug 11, 2026
08e5a05
feat(core): create dedicated spring security filter chain for webhooks
foukou19 Aug 11, 2026
10ae1f9
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 11, 2026
0141f0c
Merge branch 'feat/add-http-ingestion-route' into feat/add-custom-bas…
brandPittCode Aug 11, 2026
fa1f355
Merge branch 'main' into feat/add-http-ingestion-route
foukou19 Aug 11, 2026
2a82b8e
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 12, 2026
ef958b9
Merge branch 'feat/add-http-ingestion-route' into feat/add-custom-bas…
brandPittCode Aug 12, 2026
b656f4e
Merge branch 'main' into feat/add-custom-base64-jstl-function
brandPittCode Aug 14, 2026
03b0d25
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 14, 2026
9749e8b
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 17, 2026
6143055
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 17, 2026
f4db65a
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 17, 2026
1e0e937
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 17, 2026
21340da
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 18, 2026
6683b40
feat(core): add custom jslt function for base64 decoding
brandPittCode Aug 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
package com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt;

import java.io.StringReader;
import java.util.Collection;

import org.springframework.stereotype.Component;

import com.decathlon.idp_core.domain.exception.entity_dynamic_mapping.EntityDynamicMappingJsltErrorException;
import com.decathlon.idp_core.infrastructure.adapters.entity_mapping.engine.ExpressionEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.schibsted.spt.data.jslt.Expression;
import com.schibsted.spt.data.jslt.JsltException;
import com.schibsted.spt.data.jslt.Function;
import com.schibsted.spt.data.jslt.Parser;

@Component
public class JsltEngine implements ExpressionEngine {

private final Collection<Function> customFunctions;

// Creates a JSLT engine with the custom functions discovered by Spring.
/// @param customFunctions custom JSLT functions to register
public JsltEngine(Collection<Function> customFunctions) {
this.customFunctions = customFunctions;
}

public Expression compile(String expression) {
return new Parser(new StringReader(expression)).compile();
return Parser.compileString(expression, customFunctions);
}

@Override
public void validateExpression(String expression) {
try {
compile(expression);
} catch (JsltException exception) {
} catch (Exception exception) {
throw new EntityDynamicMappingJsltErrorException(exception.getMessage());
}
}
Expand All @@ -31,7 +39,7 @@ public void validateExpression(String expression) {
public JsonNode evaluate(String expression, JsonNode payload) {
try {
return compile(expression).apply(payload);
} catch (JsltException exception) {
} catch (Exception exception) {
Comment thread
brandPittCode marked this conversation as resolved.
throw new EntityDynamicMappingJsltErrorException(exception.getMessage());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt.functions;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.schibsted.spt.data.jslt.Function;

/// JSLT custom function that decodes a Base64-encoded string from an input
/// payload.
///
/// **Usage in JSLT:** `base64-decode(<base64-encoded-string>)`
///
/// Returns `null` if the input is absent, null, or blank.
@Component
public final class DecodeBase64 implements Function {

public static final String FUNCTION_NAME = "base64-decode";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

// Accepts exactly one argument: the Base64-encoded string node
private static final int MIN_ARGUMENTS = 1;
private static final int MAX_ARGUMENTS = 1;

@Override
public String getName() {
return FUNCTION_NAME;
}

@Override
public int getMinArguments() {
return MIN_ARGUMENTS;
}

@Override
public int getMaxArguments() {
return MAX_ARGUMENTS;
}

/// Decodes a Base64-encoded `JsonNode` string argument.
///
/// **Behavior:**
/// - Returns `NullNode` if the input is absent, null, or blank
/// - Attempts to parse the decoded string as JSON; falls back to plain text if
/// parsing fails
/// - Throws `IllegalArgumentException` if the argument is non-textual (e.g.,
/// number, object, array)
/// - Throws `IllegalArgumentException` if the Base64 string is malformed
///
/// @param input the JSLT input context (unused)
/// @param args array of JsonNode arguments; expects exactly one text node
/// containing Base64-encoded data
/// @return a `JsonNode` (either parsed JSON, plain `TextNode`, or `NullNode`)
/// @throws IllegalArgumentException if arg is not textual or contains invalid
/// Base64
@Override
public JsonNode call(JsonNode input, JsonNode[] args) {
if (args == null || args.length == 0) {
return NullNode.getInstance();
}

JsonNode arg = args[0];

// 1. Graceful: Null or absent inputs return NullNode
if (arg == null || arg.isNull()) {
return NullNode.getInstance();
}

// 2. Strict: Non-textual types (Numbers, Objects, Arrays) throw an exception
if (!arg.isTextual()) {
throw new IllegalArgumentException(
"DecodeBase64 expects a string argument, but received: " + arg.getNodeType());
}

// 3. Graceful: Blank strings return NullNode
String textValue = arg.asText();
Comment thread
brandPittCode marked this conversation as resolved.
if (textValue.isBlank()) {
return NullNode.getInstance();
}

// 4. Strict: Malformed Base64 strings throw an exception
try {
byte[] decodedBytes = Base64.getDecoder().decode(textValue);
Comment thread
brandPittCode marked this conversation as resolved.
String decodedString = new String(decodedBytes, StandardCharsets.UTF_8);
return parseDecodedString(decodedString);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid Base64 string payload: '" + textValue + "'", e);
Comment thread
brandPittCode marked this conversation as resolved.
Comment thread
brandPittCode marked this conversation as resolved.
}
}

private JsonNode parseDecodedString(String decodedString) {
// Try parsing as structured JSON first
try {
JsonNode parsed = OBJECT_MAPPER.readTree(decodedString);
// Jackson returns Java null if decodedString is empty ("")
return (parsed != null) ? parsed : TextNode.valueOf(decodedString);
} catch (JsonProcessingException _) {
// Fallback to plain TextNode if the decoded string is not JSON
return TextNode.valueOf(decodedString);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.assertj.core.api.Assertions.*;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -24,7 +25,8 @@ class JsltEntityMappingValidatorTest {

@BeforeEach
void setUp() {
validator = new JsltEntityMappingValidator(new JsltEngine());
validator = new JsltEntityMappingValidator(new JsltEngine(Collections.emptyList()));

}

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Collections;
import java.util.List;
import java.util.Map;

Expand All @@ -23,7 +24,7 @@ class JsltMappingEngineAdapterTest {

@BeforeEach
void setUp() {
var jsltEngine = new JsltEngine();
var jsltEngine = new JsltEngine(Collections.emptyList());
adapter = new JsltMappingEngineAdapter(jsltEngine, new ObjectMapper(),
new JsltExpressionEvaluator(jsltEngine));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt.functions;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import com.decathlon.idp_core.domain.exception.entity_dynamic_mapping.EntityDynamicMappingJsltErrorException;
import com.decathlon.idp_core.infrastructure.adapters.entity_mapping.jslt.JsltEngine;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.TextNode;

// Unit test for the DecodeBase64 JSLT custom function,
// covering direct invocation and integration with the JsltEngine.
@DisplayName("DecodeBase64")
class DecodeBase64Test {

private DecodeBase64 decodeBase64;
private JsltEngine jsltEngine;
private ObjectMapper objectMapper;

@BeforeEach
void setUp() {
decodeBase64 = new DecodeBase64();
jsltEngine = new JsltEngine(List.of(decodeBase64));
objectMapper = new ObjectMapper();
}

@Test
@DisplayName("Should return correct metadata signature")
void testMetadata() {
assertEquals("base64-decode", decodeBase64.getName());
assertEquals(1, decodeBase64.getMinArguments());
assertEquals(1, decodeBase64.getMaxArguments());
}

@Test
@DisplayName("Should successfully decode a valid Base64 string directly")
void testDirectDecodeValidBase64() {
// "SGVsbG8gV29ybGQ=" is Base64 for "Hello World"
JsonNode context = NullNode.getInstance();
JsonNode[] args = new JsonNode[]{TextNode.valueOf("SGVsbG8gV29ybGQ=")};

JsonNode result = decodeBase64.call(context, args);

assertNotNull(result);
assertTrue(result.isTextual());
assertEquals("Hello World", result.asText());
}

@Test
@DisplayName("Should gracefully return NullNode for null, absent, or blank inputs")
void testDirectDecodeGracefulNullAndBlank() {
JsonNode context = NullNode.getInstance();

// Null array element
JsonNode[] nullElementArgs = new JsonNode[]{null};
assertEquals(NullNode.getInstance(), decodeBase64.call(context, nullElementArgs));

// NullNode instance
JsonNode[] nullNodeArgs = new JsonNode[]{NullNode.getInstance()};
assertEquals(NullNode.getInstance(), decodeBase64.call(context, nullNodeArgs));

// Blank string
JsonNode[] blankStringArgs = new JsonNode[]{TextNode.valueOf(" ")};
assertEquals(NullNode.getInstance(), decodeBase64.call(context, blankStringArgs));
}

@Test
@DisplayName("Should throw IllegalArgumentException when input is not a textual type")
void testDirectDecodeWrongTypeThrowsException() {
JsonNode context = NullNode.getInstance();
JsonNode[] nonTextualArgs = new JsonNode[]{new IntNode(123)};

assertThrows(IllegalArgumentException.class, () -> decodeBase64.call(context, nonTextualArgs));
}

@Test
@DisplayName("Should throw IllegalArgumentException when string payload is malformed Base64")
void testDirectDecodeMalformedBase64ThrowsException() {
JsonNode context = NullNode.getInstance();
JsonNode[] malformedBase64Args = new JsonNode[]{TextNode.valueOf("NotValidBase64!!!")};

assertThrows(IllegalArgumentException.class,
() -> decodeBase64.call(context, malformedBase64Args));
}

@Test
@DisplayName("Should execute DecodeBase64 function inside JSLT engine evaluation successfully")
void testJsltEngineIntegrationSuccess() throws Exception {
String jsltExpression = "{ \"decoded\": base64-decode(.encoded_data) }";
JsonNode payload = objectMapper.readTree("{\"encoded_data\": \"SGVsbG8=\"}"); // "SGVsbG8=" ->
// "Hello"

JsonNode result = jsltEngine.evaluate(jsltExpression, payload);

assertNotNull(result);
assertNotNull(result.get("decoded"));
assertTrue(result.get("decoded").isTextual());
assertEquals("Hello", result.get("decoded").asText());
}

@Test
@DisplayName("Should wrap Base64 decoding exception in EntityDynamicMappingJsltErrorException during engine evaluation")
void testJsltEngineIntegrationFailure() throws Exception {
String jsltExpression = "{ \"decoded\": base64-decode(.encoded_data) }";
JsonNode payloadWithInvalidBase64 = objectMapper
.readTree("{\"encoded_data\": \"NotValidBase64!!!\"}");

assertThrows(EntityDynamicMappingJsltErrorException.class,
() -> jsltEngine.evaluate(jsltExpression, payloadWithInvalidBase64));
}
}
Loading