Skip to content

Commit

Permalink
provide a new function to join placeholder array pipeline elements in…
Browse files Browse the repository at this point in the history
…to a single string

Signed-off-by: Thomas Jäckle <thomas.jaeckle@beyonnex.io>
  • Loading branch information
thjaeckle committed Sep 19, 2023
1 parent e5f32fd commit 868916d
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 2 deletions.
Expand Up @@ -291,6 +291,7 @@ The following functions are provided by Ditto out of the box:
| `fn:delete` | `()` | Deletes the result of the previous pipeline expression unconditionally. Any following expressions are ignored. | `fn:delete()` |
| `fn:replace` | `(String from, String to)` | Replaces a string with another using Java's `String::replace` method. | `fn:replace('foo', 'bar')` |
| `fn:split` | `(String separator)` | Splits the previous pipeline using the passed `separator` resulting an "array" pipeline output containing several elements.<br/>May only be used in combination with the [JWT placeholder](#scope-openid-connect-configuration) as input placeholder. | `fn:split(' ')`<br/>`fn:split(',')` |
| `fn:join` | `(String delimiter)` | Joins the previous pipeline elements (when they were an array) resulting a single string pipeline output delimited with the passed `delimiter`. | `fn:join(':')`<br/>`fn:join(',')` |

### RQL functions

Expand Down
Expand Up @@ -44,7 +44,8 @@ final class ImmutableFunctionExpression implements FunctionExpression {
new PipelineFunctionBase64Decode(), // fn:base64-decode()
new PipelineFunctionDelete(), // fn:delete()
new PipelineFunctionReplace(), // fn:replace('from', 'to')
new PipelineFunctionSplit() // fn:split(' ')
new PipelineFunctionSplit(), // fn:split(' ')
new PipelineFunctionJoin() // fn:join(',')
));

@Override
Expand Down
@@ -0,0 +1,114 @@
/*
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.placeholders;

import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import javax.annotation.concurrent.Immutable;

/**
* Provides the {@code fn:join('delimiter')} function implementation.
*/
@Immutable
final class PipelineFunctionJoin implements PipelineFunction {

private static final String FUNCTION_NAME = "join";

private final PipelineFunctionParameterResolverFactory.SingleParameterResolver parameterResolver =
PipelineFunctionParameterResolverFactory.forStringParameter();

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

@Override
public Signature getSignature() {
return JoinFunctionSignature.INSTANCE;
}

@Override
public PipelineElement apply(final PipelineElement value, final String paramsIncludingParentheses,
final ExpressionResolver expressionResolver) {

final String joinDelimiter = parseAndResolve(paramsIncludingParentheses, expressionResolver);
if (value.toStream().count() == 0) {
return PipelineElement.unresolved();
} else {
return PipelineElement.resolved(
value.toStream().collect(Collectors.joining(joinDelimiter))
);
}
}

private String parseAndResolve(final String paramsIncludingParentheses,
final ExpressionResolver expressionResolver) {

return parameterResolver.apply(paramsIncludingParentheses, expressionResolver, this)
.findFirst()
.orElseThrow(
() -> PlaceholderFunctionSignatureInvalidException.newBuilder(paramsIncludingParentheses, this)
.build());
}

/**
* Describes the signature of the {@code join('delimiter')} function.
*/
private static final class JoinFunctionSignature implements Signature {

private static final JoinFunctionSignature INSTANCE = new JoinFunctionSignature();

private final ParameterDefinition<String> givenStringDescription;

private JoinFunctionSignature() {
givenStringDescription = new GivenStringParam();
}

@Override
public List<ParameterDefinition<?>> getParameterDefinitions() {
return Collections.singletonList(givenStringDescription);
}

@Override
public String toString() {
return renderSignature();
}
}

/**
* Describes the only param of the {@code join('delimiter')} function.
*/
private static final class GivenStringParam implements ParameterDefinition<String> {

private GivenStringParam() {
}

@Override
public String getName() {
return "delimiter";
}

@Override
public Class<String> getType() {
return String.class;
}

@Override
public String getDescription() {
return "Specifies the string to use as delimiter when joining elements of an array to a string";
}
}

}
Expand Up @@ -47,7 +47,8 @@ public class ImmutableFunctionExpressionTest {
"base64-decode",
"delete",
"replace",
"split"
"split",
"join"
)));
private static final HeadersPlaceholder HEADERS_PLACEHOLDER = PlaceholderFactory.newHeadersPlaceholder();

Expand Down
@@ -0,0 +1,88 @@
/*
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.placeholders;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.Mockito.verifyNoInteractions;

import java.util.Arrays;
import java.util.Collections;

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class PipelineFunctionJoinTest {

private static final PipelineElement EMPTY_INPUT = PipelineElement.unresolved();

private static final PipelineElement KNOWN_INPUT = PipelineElement.resolved(Arrays.asList("foo","bar","baz"));
private static final String EXPECTED_OUTPUT = "foo:bar:baz";

private static final PipelineElement KNOWN_INPUT_NO_MATCH = PipelineElement.resolved(
Collections.singleton("unjoinable"));
private static final String EXPECTED_OUTPUT_NO_MATCH = "unjoinable";

private final PipelineFunctionJoin function = new PipelineFunctionJoin();

@Mock
private ExpressionResolver expressionResolver;

@After
public void verifyExpressionResolverUnused() {
verifyNoInteractions(expressionResolver);
}

@Test
public void getName() {
assertThat(function.getName()).isEqualTo("join");
}

@Test
public void apply() {
assertThat(function.apply(KNOWN_INPUT, "(':')", expressionResolver).toStream())
.containsOnly(EXPECTED_OUTPUT);
}

@Test
public void applyNoMatch() {
assertThat(function.apply(KNOWN_INPUT_NO_MATCH, "(':')", expressionResolver).toStream())
.containsOnly(EXPECTED_OUTPUT_NO_MATCH);
}

@Test
public void returnsEmptyForEmptyInput() {
assertThat(function.apply(EMPTY_INPUT, "(':')", expressionResolver)).isEmpty();
}

@Test
public void throwsOnInvalidParameters() {
// has not enough parameters
assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class).isThrownBy(() ->
function.apply(KNOWN_INPUT, "()", expressionResolver)
);
// has too many parameters
assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class).isThrownBy(() ->
function.apply(KNOWN_INPUT, "(' ',',')", expressionResolver)
);
// has no parameters
assertThatExceptionOfType(PlaceholderFunctionSignatureInvalidException.class).isThrownBy(() ->
function.apply(KNOWN_INPUT, "", expressionResolver)
);
}

}

0 comments on commit 868916d

Please sign in to comment.