From 37cd30b1cba301db096dc545672f36e99db9aee0 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sat, 4 Jan 2025 13:23:14 -0500 Subject: [PATCH] Added support for the Metapath function map:for-each. Resolves #301 --- .../metapath/cst/AnonymousFunctionCall.java | 5 +- .../core/metapath/function/IFunction.java | 10 ++ .../library/DefaultFunctionLibrary.java | 3 +- .../metapath/function/library/MapForEach.java | 115 ++++++++++++++++++ .../function/library/MapForEachTest.java | 51 ++++++++ 5 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEach.java create mode 100644 core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEachTest.java diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/cst/AnonymousFunctionCall.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/cst/AnonymousFunctionCall.java index 7e5606f6f..9ffbea743 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/cst/AnonymousFunctionCall.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/cst/AnonymousFunctionCall.java @@ -146,7 +146,10 @@ protected ISequence executeInternal( subContext.bindVariableValue(param.getName(), ObjectUtils.notNull(sequence)); } - return body.accept(subContext, ISequence.of(focus)); + // the focus is not present according to + // https://www.w3.org/TR/xpath-31/#id-eval-function-call + // paragraph 5.b.ii + return body.accept(subContext, ISequence.empty()); } } } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/IFunction.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/IFunction.java index b58663e35..bc8a01d04 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/IFunction.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/IFunction.java @@ -70,6 +70,16 @@ enum FunctionProperty { UNBOUNDED_ARITY; } + /** + * Get the type information for this item. + * + * @return the type information + */ + @NonNull + static IItemType type() { + return IItemType.function(); + } + @Override default IItemType getType() { // TODO: implement this based on the signature diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java index 3b2ab0c21..a6f5b70f3 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/DefaultFunctionLibrary.java @@ -268,7 +268,8 @@ public DefaultFunctionLibrary() { // NOPMD - intentional registerFunction(MapEntry.SIGNATURE); // https://www.w3.org/TR/xpath-functions-31/#func-map-remove registerFunction(MapRemove.SIGNATURE); - // P3: https://www.w3.org/TR/xpath-functions-31/#func-map-for-each + // https://www.w3.org/TR/xpath-functions-31/#func-map-for-each + registerFunction(MapForEach.SIGNATURE); // metapath casting functions DataTypeService.instance().getDataTypes().stream() diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEach.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEach.java new file mode 100644 index 000000000..fbee039ae --- /dev/null +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEach.java @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import gov.nist.secauto.metaschema.core.metapath.DynamicContext; +import gov.nist.secauto.metaschema.core.metapath.MetapathConstants; +import gov.nist.secauto.metaschema.core.metapath.function.FunctionUtils; +import gov.nist.secauto.metaschema.core.metapath.function.IArgument; +import gov.nist.secauto.metaschema.core.metapath.function.IFunction; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; +import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem; +import gov.nist.secauto.metaschema.core.metapath.item.function.IMapItem; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; + +import java.util.List; +import java.util.function.BiFunction; + +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Implements the XPath 3.1 map:contains + * function. + */ +public final class MapForEach { + private static final String NAME = "for-each"; + @NonNull + static final IFunction SIGNATURE = IFunction.builder() + .name(NAME) + .namespace(MetapathConstants.NS_METAPATH_FUNCTIONS_MAP) + .deterministic() + .contextIndependent() + .focusIndependent() + .argument(IArgument.builder() + .name("map") + .type(IMapItem.type()) + .one() + .build()) + .argument(IArgument.builder() + .name("action") + .type(IFunction.type()) + .one() + .build()) + .returnType(IItem.type()) + .returnOne() + .functionHandler(MapForEach::execute) + .build(); + + private MapForEach() { + // disable construction + } + + @SuppressWarnings("unused") + @NonNull + private static ISequence execute(@NonNull IFunction function, + @NonNull List> arguments, + @NonNull DynamicContext dynamicContext, + IItem focus) { + IMapItem map = FunctionUtils.asType(ObjectUtils.requireNonNull(arguments.get(0).getFirstItem(true))); + IFunction action = FunctionUtils.asType(ObjectUtils.requireNonNull(arguments.get(1).getFirstItem(true))); + + BiFunction, ISequence> lambda = adaptFunction(action, dynamicContext); + return forEach(map, lambda); + } + + /** + * Adapts the provided function call to a lambda expression for use with + * {@link #forEach(IMapItem, BiFunction)}. + * + * @param function + * the function to adapt + * @param dynamicContext + * the dynamic context for use in evaluating the function + * @return + */ + @NonNull + private static BiFunction, ISequence> adaptFunction( + @NonNull IFunction function, + @NonNull DynamicContext dynamicContext) { + return (key, value) -> { + List> arguments = ObjectUtils.notNull(List.of( + ISequence.of(key), + value.toSequence())); + + return function.execute(arguments, dynamicContext, ISequence.empty()); + }; + } + + /** + * An implementation of XPath 3.1 map:for-each. + * + * @param map + * the map of Metapath items that is the target of retrieval + * @param action + * the action to perform on each entry in the map + * @return a stream resulting from concatenating the resulting streams provided + * by the action result + */ + @NonNull + public static ISequence forEach( + @NonNull IMapItem map, + @NonNull BiFunction, ISequence> action) { + return ISequence.of(ObjectUtils.notNull(map.entrySet().stream() + .flatMap(entry -> { + IAnyAtomicItem key = entry.getKey().getKey(); + ISequence values = entry.getValue().contentsAsSequence(); + return action.apply(key, values).stream(); + }))); + } +} diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEachTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEachTest.java new file mode 100644 index 000000000..6453362ef --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/metapath/function/library/MapForEachTest.java @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.metapath.function.library; + +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.entry; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.integer; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.map; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.sequence; +import static gov.nist.secauto.metaschema.core.metapath.TestUtils.string; +import static org.assertj.core.api.Assertions.assertThat; + +import gov.nist.secauto.metaschema.core.metapath.ExpressionTestBase; +import gov.nist.secauto.metaschema.core.metapath.IMetapathExpression; +import gov.nist.secauto.metaschema.core.metapath.item.IItem; +import gov.nist.secauto.metaschema.core.metapath.item.ISequence; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import edu.umd.cs.findbugs.annotations.NonNull; + +class MapForEachTest + extends ExpressionTestBase { + + private static Stream provideValues() { // NOPMD - false positive + return Stream.of( + Arguments.of( + sequence(integer(1), integer(2)), + "map:for-each(map{1:'yes', 2:'no'}, function($k, $v){$k})"), + Arguments.of( + sequence(string("yes"), string("no")), + "distinct-values(map:for-each(map{1:'yes', 2:'no'}, function($k, $v){$v}))"), + Arguments.of( + sequence(map(entry(string("a"), integer(2)), entry(string("b"), integer(3)))), + "map:merge(map:for-each(map{'a':1, 'b':2}, function($k, $v){map:entry($k, $v+1)}))")); + } + + @ParameterizedTest + @MethodSource("provideValues") + void testExpression(@NonNull ISequence expected, @NonNull String metapath) { + + ISequence result = IMetapathExpression.compile(metapath).evaluate(null, newDynamicContext()); + assertThat(result).containsAll(expected); + } +}