From 98a2c539ffc689e4be8c6c9b9221e0ffa93f9aa3 Mon Sep 17 00:00:00 2001 From: David Waltermire Date: Sun, 27 Oct 2024 14:00:04 -0400 Subject: [PATCH] Support for remarks in let statements (#216) * Added support for remarks in let statements. resolves #169. --- .../function/library/FnUpperCase.java | 1 - .../core/model/constraint/ILet.java | 31 +++++++-- .../core/model/constraint/ISource.java | 6 ++ .../model/constraint/impl/DefaultLet.java | 20 +++++- .../core/model/xml/impl/ModelFactory.java | 6 +- .../core/model/xml/impl/ModelFactoryTest.java | 49 +++++++++++++ .../computer-metaschema-meta-constraints.xml | 6 +- .../codegen/impl/AnnotationGenerator.java | 10 ++- .../databind/model/annotations/Let.java | 4 ++ .../model/impl/ConstraintFactory.java | 11 ++- .../impl/ConstraintBindingSupport.java | 61 +++++++++++++++- .../databind/codegen/BasicMetaschemaTest.java | 1 - .../codegen/impl/AnnotationGeneratorTest.java | 69 +++++++++++++++++++ .../model/impl/ConstraintFactoryTest.java | 63 +++++++++++++++++ 14 files changed, 319 insertions(+), 19 deletions(-) create mode 100644 core/src/test/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactoryTest.java create mode 100644 databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java create mode 100644 databind/src/test/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactoryTest.java diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnUpperCase.java b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnUpperCase.java index 173a4d69b..9e199156f 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnUpperCase.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/metapath/function/library/FnUpperCase.java @@ -60,7 +60,6 @@ private static ISequence executeOneArg(@NonNull IFunction function, return ISequence.of(arguments.get(0).isEmpty() ? IStringItem.valueOf("") : fnUpperCase(FunctionUtils.asType(ObjectUtils.notNull(arguments.get(0).getFirstItem(true))))); - } /** diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ILet.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ILet.java index 7e81bd7d5..6b4690f38 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ILet.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ILet.java @@ -5,6 +5,7 @@ package gov.nist.secauto.metaschema.core.model.constraint; +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; import gov.nist.secauto.metaschema.core.metapath.MetapathException; import gov.nist.secauto.metaschema.core.metapath.MetapathExpression; import gov.nist.secauto.metaschema.core.model.constraint.impl.DefaultLet; @@ -12,7 +13,11 @@ import javax.xml.namespace.QName; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +/** + * Represents a variable assignment for use in Metaschema module constraints. + */ @SuppressWarnings("PMD.ShortClassName") public interface ILet { /** @@ -25,6 +30,8 @@ public interface ILet { * a Metapath expression string representing the variable value * @param source * the source descriptor for the resource containing the constraint + * @param remarks + * remarks about the let statement * @return the original let statement with the same name or {@code null} */ @SuppressWarnings("PMD.ShortMethodName") @@ -32,9 +39,14 @@ public interface ILet { static ILet of( @NonNull QName name, @NonNull String valueExpression, - @NonNull ISource source) { + @NonNull ISource source, + @Nullable MarkupMultiline remarks) { try { - return of(name, MetapathExpression.compile(valueExpression, source.getStaticContext()), source); + return of( + name, + MetapathExpression.compile(valueExpression, source.getStaticContext()), + source, + remarks); } catch (MetapathException ex) { throw new MetapathException( String.format("Unable to compile the let expression '%s=%s'%s. %s", @@ -55,6 +67,8 @@ static ILet of( * a Metapath expression representing the variable value * @param source * the source descriptor for the resource containing the constraint + * @param remarks + * remarks about the let statement * @return the original let statement with the same name or {@code null} */ @SuppressWarnings("PMD.ShortMethodName") @@ -62,8 +76,9 @@ static ILet of( static ILet of( @NonNull QName name, @NonNull MetapathExpression valueExpression, - @NonNull ISource source) { - return new DefaultLet(name, valueExpression, source); + @NonNull ISource source, + @Nullable MarkupMultiline remarks) { + return new DefaultLet(name, valueExpression, source, remarks); } /** @@ -89,4 +104,12 @@ static ILet of( */ @NonNull ISource getSource(); + + /** + * Get the remarks associated with the let statement. + * + * @return the remark or {@code null} if no remarks are defined + */ + @Nullable + MarkupMultiline getRemarks(); } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ISource.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ISource.java index 7de6f88be..91ded6b5b 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ISource.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/ISource.java @@ -48,6 +48,9 @@ static ISource modelSource(@NonNull IModule module) { * Get the descriptor for a * {@link gov.nist.secauto.metaschema.core.model.constraint.ISource.SourceType#EXTERNAL} * source with as associated resource. + *

+ * The provided static context idenfies the location of this source based on the + * {@link StaticContext#getBaseUri()} method. * * @param staticContext * the static Metapath context to use for compiling Metapath @@ -57,6 +60,9 @@ static ISource modelSource(@NonNull IModule module) { */ @NonNull static ISource externalSource(@NonNull StaticContext staticContext) { + if (staticContext.getBaseUri() == null) { + throw new IllegalArgumentException("The static content must define a baseUri identifing the source resource."); + } return ExternalSource.instance(staticContext); } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultLet.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultLet.java index e5311b2db..e4e214d69 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultLet.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/constraint/impl/DefaultLet.java @@ -5,6 +5,7 @@ package gov.nist.secauto.metaschema.core.model.constraint.impl; +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; import gov.nist.secauto.metaschema.core.metapath.MetapathExpression; import gov.nist.secauto.metaschema.core.model.constraint.ILet; import gov.nist.secauto.metaschema.core.model.constraint.ISource; @@ -12,7 +13,13 @@ import javax.xml.namespace.QName; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +/** + * A variable assignment for use in Metaschema module constraints. + *

+ * This class is immutable. + */ public class DefaultLet implements ILet { @NonNull private final QName name; @@ -20,6 +27,8 @@ public class DefaultLet implements ILet { private final MetapathExpression valueExpression; @NonNull private final ISource source; + @Nullable + private final MarkupMultiline remarks; /** * Construct a new let statement. @@ -30,14 +39,18 @@ public class DefaultLet implements ILet { * the Metapath expression used to query the value * @param source * the source of the let statement + * @param remarks + * remarks about the let statement */ public DefaultLet( @NonNull QName name, @NonNull MetapathExpression metapath, - @NonNull ISource source) { + @NonNull ISource source, + @Nullable MarkupMultiline remarks) { this.name = name; this.valueExpression = metapath; this.source = source; + this.remarks = remarks; } @Override @@ -55,4 +68,9 @@ public MetapathExpression getValueExpression() { public ISource getSource() { return source; } + + @Override + public MarkupMultiline getRemarks() { + return remarks; + } } diff --git a/core/src/main/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactory.java b/core/src/main/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactory.java index e207b887d..083c60144 100644 --- a/core/src/main/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactory.java +++ b/core/src/main/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactory.java @@ -471,10 +471,14 @@ public static ICardinalityConstraint newCardinalityConstraint( public static ILet newLet( @NonNull ConstraintLetType xmlObject, @NonNull ISource source) { + // TODO: figure out how to resolve the namespace prefix on var return ILet.of( new QName(xmlObject.getVar()), ObjectUtils.notNull(xmlObject.getExpression()), - source); + source, + xmlObject.isSetRemarks() + ? remarks(ObjectUtils.notNull(xmlObject.getRemarks())) + : null); } } diff --git a/core/src/test/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactoryTest.java b/core/src/test/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactoryTest.java new file mode 100644 index 000000000..d17a8a069 --- /dev/null +++ b/core/src/test/java/gov/nist/secauto/metaschema/core/model/xml/impl/ModelFactoryTest.java @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.core.model.xml.impl; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import gov.nist.secauto.metaschema.core.metapath.StaticContext; +import gov.nist.secauto.metaschema.core.model.constraint.ILet; +import gov.nist.secauto.metaschema.core.model.constraint.ISource; +import gov.nist.secauto.metaschema.core.model.xml.xmlbeans.ConstraintLetType; +import gov.nist.secauto.metaschema.core.model.xml.xmlbeans.RemarksType; + +import org.apache.xmlbeans.XmlException; +import org.junit.jupiter.api.Test; + +import java.net.URI; + +import javax.xml.namespace.QName; + +class ModelFactoryTest { + + @SuppressWarnings("null") + @Test + void letTest() throws XmlException { + ISource source = ISource.externalSource(StaticContext.builder() + .baseUri(URI.create("https://example.com/")) + .build()); + + String variable = "var1"; + String expression = "1 + 1"; + RemarksType remarks = RemarksType.Factory.parse("

Test

"); + + ConstraintLetType letObj = ConstraintLetType.Factory.newInstance(); + letObj.setExpression(expression); + letObj.setVar(variable); + letObj.setRemarks(remarks); + + ILet let = ModelFactory.newLet(letObj, source); + assertAll( + () -> assertEquals(new QName(variable), let.getName()), + () -> assertEquals(expression, let.getValueExpression().getPath()), + () -> assertEquals(source, let.getSource()), + () -> assertEquals("Test", let.getRemarks().toMarkdown())); + } +} diff --git a/core/src/test/resources/computer-metaschema-meta-constraints.xml b/core/src/test/resources/computer-metaschema-meta-constraints.xml index 3bd312ef3..6554d6c9c 100644 --- a/core/src/test/resources/computer-metaschema-meta-constraints.xml +++ b/core/src/test/resources/computer-metaschema-meta-constraints.xml @@ -7,7 +7,11 @@ - + + +

This matches the id of child computer and vendor elements.

+
+
\ No newline at end of file diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java index c1da156be..ef49d3697 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGenerator.java @@ -194,12 +194,10 @@ private static void applyLetAssignments( letAnnotation.addMember("name", "$S", let.getName()); letAnnotation.addMember("target", "$S", let.getValueExpression().getPath()); - // TODO: Support remarks - // MarkupMultiline remarks = let.getRemarks(); - // if (remarks != null) { - // constraintAnnotation.addMember("remarks", "$S", - // remarks.toMarkdown()); - // } + MarkupMultiline remarks = let.getRemarks(); + if (remarks != null) { + letAnnotation.addMember("remarks", "$S", remarks.toMarkdown()); + } annotation.addMember("lets", "$L", letAnnotation.build()); } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Let.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Let.java index 8ea45d319..615230003 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Let.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/annotations/Let.java @@ -14,6 +14,10 @@ import edu.umd.cs.findbugs.annotations.NonNull; +/** + * Represents a constraint let statement used to assign the result of a Metapath + * expression to a variable. + */ @Documented @Retention(RUNTIME) @Target(ElementType.ANNOTATION_TYPE) diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java index 56d58ce49..45f7bdae3 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactory.java @@ -48,6 +48,7 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +@SuppressWarnings("PMD.CouplingBetweenObjects") final class ConstraintFactory { private ConstraintFactory() { // disable @@ -324,6 +325,14 @@ static ICardinalityConstraint newCardinalityConstraint(@NonNull HasCardinality c @NonNull static ILet newLetExpression(@NonNull Let annotation, @NonNull ISource source) { - return ILet.of(new QName(annotation.name()), annotation.target(), source); + String remarkMarkdown = annotation.remarks(); + MarkupMultiline remarks = remarkMarkdown.isBlank() + ? null + : MarkupMultiline.fromMarkdown(remarkMarkdown); + return ILet.of( + new QName(annotation.name()), + annotation.target(), + source, + remarks); } } diff --git a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java index 9c43a266c..506253aa8 100644 --- a/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java +++ b/databind/src/main/java/gov/nist/secauto/metaschema/databind/model/metaschema/impl/ConstraintBindingSupport.java @@ -7,6 +7,7 @@ import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter; import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine; +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; import gov.nist.secauto.metaschema.core.model.constraint.AbstractConstraintBuilder; import gov.nist.secauto.metaschema.core.model.constraint.AbstractKeyConstraintBuilder; import gov.nist.secauto.metaschema.core.model.constraint.IAllowedValuesConstraint; @@ -52,11 +53,25 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +/** + * Supports parsing constraints declared within a bound object. + */ +@SuppressWarnings("PMD.CouplingBetweenObjects") public final class ConstraintBindingSupport { private ConstraintBindingSupport() { // disable construction } + /** + * Parse a constraint set. + * + * @param constraintSet + * the parsed constraint set + * @param constraints + * the constraint definitions to parse + * @param source + * the source of the constraints + */ public static void parse( @NonNull IValueConstrained constraintSet, @NonNull IValueConstraintsBase constraints, @@ -81,6 +96,16 @@ public static void parse( } } + /** + * Parse a constraint set. + * + * @param constraintSet + * the parsed constraint set + * @param constraints + * the constraint definitions to parse + * @param source + * the source of the constraints + */ public static void parse( @NonNull IValueConstrained constraintSet, @NonNull IValueTargetedConstraintsBase constraints, @@ -105,6 +130,16 @@ public static void parse( } } + /** + * Parse a constraint set. + * + * @param constraintSet + * the parsed constraint set + * @param constraints + * the constraint definitions to parse + * @param source + * the source of the constraints + */ public static void parse( @NonNull IModelConstrained constraintSet, @NonNull IModelConstraintsBase constraints, @@ -138,15 +173,35 @@ public static void parse( } } + /** + * Parse the let clause in a constraint set. + * + * @param constraintSet + * the parsed constraint set + * @param constraints + * the constraint definitions to parse + * @param source + * the source of the constraint + */ public static void parseLet( @NonNull IValueConstrained constraintSet, @NonNull IValueConstraintsBase constraints, @NonNull ISource source) { // parse let expressions constraints.getLets().stream() - .map(letObj -> ILet.of( - ObjectUtils.requireNonNull(new QName(letObj.getVar())), - ObjectUtils.requireNonNull(letObj.getExpression()), source)) + .map(letObj -> { + MarkupMultiline remarks = null; + Remarks remarkObj = letObj.getRemarks(); + if (remarkObj != null) { + remarks = remarkObj.getRemark(); + } + + return ILet.of( + ObjectUtils.requireNonNull(new QName(letObj.getVar())), + ObjectUtils.requireNonNull(letObj.getExpression()), + source, + remarks); + }) .forEachOrdered(constraintSet::addLetExpression); } diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/BasicMetaschemaTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/BasicMetaschemaTest.java index a4af3b95e..b99875eef 100644 --- a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/BasicMetaschemaTest.java +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/BasicMetaschemaTest.java @@ -223,7 +223,6 @@ void testExistsWithVariable() throws IOException, URISyntaxException, Metaschema } @Test - @SuppressWarnings("unchecked") void codegenTest() throws MetaschemaException, IOException { List constraints; diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java new file mode 100644 index 000000000..05d6b8bd5 --- /dev/null +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/codegen/impl/AnnotationGeneratorTest.java @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.databind.codegen.impl; + +import com.squareup.javapoet.AnnotationSpec; + +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; +import gov.nist.secauto.metaschema.core.metapath.StaticContext; +import gov.nist.secauto.metaschema.core.model.IFlagDefinition; +import gov.nist.secauto.metaschema.core.model.constraint.ILet; +import gov.nist.secauto.metaschema.core.model.constraint.ISource; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; +import gov.nist.secauto.metaschema.databind.model.annotations.BoundFlag; + +import org.jmock.Expectations; +import org.jmock.junit5.JUnit5Mockery; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import javax.xml.namespace.QName; + +class AnnotationGeneratorTest { + @RegisterExtension + final JUnit5Mockery context = new JUnit5Mockery(); + + @Test + void letAssignmentTest() { + ISource source = ISource.externalSource(StaticContext.builder() + .baseUri(ObjectUtils.notNull(URI.create("https://example.com/"))) + .build()); + + String variable = "var1"; + String expression = "1 + 1"; + MarkupMultiline remarks = MarkupMultiline.fromMarkdown("Test"); + + ILet let = ILet.of( + new QName(variable), + expression, + source, + remarks); + + AnnotationSpec.Builder annotation = ObjectUtils.notNull(AnnotationSpec.builder(BoundFlag.class)); + IFlagDefinition flag = ObjectUtils.notNull(context.mock(IFlagDefinition.class)); + + context.checking(new Expectations() { + { + allowing(flag).getLetExpressions(); + will(returnValue(Map.ofEntries(Map.entry(let.getName(), let)))); + allowing(flag).getAllowedValuesConstraints(); + will(returnValue(List.of())); + allowing(flag).getIndexHasKeyConstraints(); + will(returnValue(List.of())); + allowing(flag).getMatchesConstraints(); + will(returnValue(List.of())); + allowing(flag).getExpectConstraints(); + will(returnValue(List.of())); + } + }); + + AnnotationGenerator.buildValueConstraints(annotation, flag); + } +} diff --git a/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactoryTest.java b/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactoryTest.java new file mode 100644 index 000000000..cd9d2ce4c --- /dev/null +++ b/databind/src/test/java/gov/nist/secauto/metaschema/databind/model/impl/ConstraintFactoryTest.java @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: none + * SPDX-License-Identifier: CC0-1.0 + */ + +package gov.nist.secauto.metaschema.databind.model.impl; + +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline; +import gov.nist.secauto.metaschema.core.metapath.StaticContext; +import gov.nist.secauto.metaschema.core.model.constraint.ILet; +import gov.nist.secauto.metaschema.core.model.constraint.ISource; +import gov.nist.secauto.metaschema.core.util.ObjectUtils; +import gov.nist.secauto.metaschema.databind.model.annotations.Let; + +import org.jmock.Expectations; +import org.jmock.junit5.JUnit5Mockery; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import java.net.URI; + +import javax.xml.namespace.QName; + +class ConstraintFactoryTest { + @RegisterExtension + final JUnit5Mockery context = new JUnit5Mockery(); + + @SuppressWarnings("null") + @Test + void letExpressionTest() { + ISource source = ISource.externalSource(StaticContext.builder() + .baseUri(ObjectUtils.notNull(URI.create("https://example.com/"))) + .build()); + + String variable = "var1"; + String expression = "1 + 1"; + MarkupMultiline remarks = MarkupMultiline.fromMarkdown("Test"); + + Let annotation = context.mock(Let.class); + + context.checking(new Expectations() { + { + allowing(annotation).name(); + will(returnValue(variable)); + allowing(annotation).target(); + will(returnValue(expression)); + allowing(annotation).remarks(); + will(returnValue(remarks.toMarkdown())); + } + }); + + ILet let = ConstraintFactory.newLetExpression(annotation, source); + assertAll( + () -> assertEquals(new QName(variable), let.getName()), + () -> assertEquals(expression, let.getValueExpression().getPath()), + () -> assertEquals(source, let.getSource()), + () -> assertEquals("Test", let.getRemarks().toMarkdown())); + } + +}