This is an automated email from the ASF dual-hosted git repository.

ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/isis.git


The following commit(s) were added to refs/heads/master by this push:
     new 1ade9a86e8 ISIS-3121: fixes BigDecimal fractional digit rendering when 
constraint
1ade9a86e8 is described below

commit 1ade9a86e8ac2be2a0525e20f7d3fcf561e0dc95
Author: Andi Huber <[email protected]>
AuthorDate: Wed Aug 24 13:43:28 2022 +0200

    ISIS-3121: fixes BigDecimal fractional digit rendering when constraint
---
 .../value/semantics/ValueSemanticsAbstract.java    |  54 ++++++---
 .../managed/nonscalar/DataTableModel.java          |   1 +
 .../apache/isis/core/metamodel/util/Facets.java    |  10 ++
 .../valuesemantics/BigDecimalValueSemantics.java   |  27 +++--
 .../bigdecimals/jdo/JavaMathBigDecimalJdo.java     |  10 ++
 .../bigdecimals/jpa/JavaMathBigDecimalJpa.java     |  10 ++
 .../isis/testdomain/value/ValueSemanticsTest.java  |  70 ++++++++++-
 .../model/valuetypes/ValueTypeExample.java         | 130 ++++++++++++++++++++-
 .../model/valuetypes/ValueTypeExampleService.java  |   2 +-
 9 files changed, 274 insertions(+), 40 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
 
b/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
index 5346b132fc..506c858784 100644
--- 
a/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
+++ 
b/api/applib/src/main/java/org/apache/isis/applib/value/semantics/ValueSemanticsAbstract.java
@@ -63,7 +63,7 @@ import lombok.val;
  */
 public abstract class ValueSemanticsAbstract<T>
 implements
-    ValueSemanticsProvider<T> {
+ValueSemanticsProvider<T> {
 
     @SuppressWarnings("unchecked")
     @Override
@@ -112,9 +112,9 @@ implements
      */
     protected UserLocale getUserLocale(final @Nullable 
ValueSemanticsProvider.Context context) {
         return Optional.ofNullable(context)
-        .map(ValueSemanticsProvider.Context::getInteractionContext)
-        .map(InteractionContext::getLocale)
-        .orElseGet(UserLocale::getDefault);
+                .map(ValueSemanticsProvider.Context::getInteractionContext)
+                .map(InteractionContext::getLocale)
+                .orElseGet(UserLocale::getDefault);
     }
 
     protected String renderTitle(final T value, final Function<T, String> 
toString) {
@@ -139,8 +139,8 @@ implements
         _Assert.assertEquals(getSchemaValueType(), ValueType.STRING);
         return CommonDtoUtils.fundamentalTypeAsDecomposition(ValueType.STRING,
                 value!=null
-                    ? toString.apply(value)
-                    : onNull.get());
+                ? toString.apply(value)
+                : onNull.get());
     }
 
     protected T composeFromString(
@@ -149,7 +149,7 @@ implements
             final @NonNull Supplier<T> onNullOrEmpty) {
         val string = decomposition!=null
                 ? 
decomposition.left().map(ValueWithTypeDto::getString).orElse(null)
-                : null;
+                        : null;
         return _Strings.isNotEmpty(string)
                 ? fromString.apply(string)
                 : onNullOrEmpty.get();
@@ -165,8 +165,8 @@ implements
             final @NonNull Supplier<F> onNull) {
         return 
CommonDtoUtils.fundamentalTypeAsDecomposition(getSchemaValueType(),
                 value!=null
-                    ? onNonNull.apply(value)
-                    : onNull.get());
+                ? onNonNull.apply(value)
+                : onNull.get());
     }
 
     /**
@@ -191,7 +191,7 @@ implements
 
     /**
      * @param context - nullable in support of JUnit testing
-     * @return {@link NumberFormat} the default from from given context's 
locale
+     * @return {@link NumberFormat} the default from given context's locale
      * or else system's default locale
      *
      * @implNote the format's MaximumFractionDigits are initialized to 16, as
@@ -199,11 +199,19 @@ implements
      * this is typically overruled later by implementations of
      * {@link 
#configureDecimalFormat(org.apache.isis.applib.adapters.ValueSemanticsProvider.Context,
 DecimalFormat) configureDecimalFormat}
      */
-   @SuppressWarnings("javadoc")
-   protected DecimalFormat getNumberFormat(final @Nullable 
ValueSemanticsProvider.Context context) {
+    @SuppressWarnings("javadoc")
+    protected DecimalFormat getNumberFormat(
+            final @Nullable ValueSemanticsProvider.Context context) {
+        return getNumberFormat(context, FormatUsageFor.RENDERING);
+    }
+
+    protected DecimalFormat getNumberFormat(
+            final @Nullable ValueSemanticsProvider.Context context,
+            final @NonNull FormatUsageFor usedFor) {
         val format = 
(DecimalFormat)NumberFormat.getNumberInstance(getUserLocale(context).getNumberFormatLocale());
         // prime w/ 16 (64 bit IEEE 754 double has 15 decimal digits of 
precision)
         format.setMaximumFractionDigits(16);
+        configureDecimalFormat(context, format, usedFor);
         return format;
     }
 
@@ -216,7 +224,7 @@ implements
         }
         try {
             return parseDecimal(context, input)
-            .map(BigDecimal::toBigIntegerExact);
+                    .map(BigDecimal::toBigIntegerExact);
         } catch (final NumberFormatException | ArithmeticException e) {
             throw new TextEntryParseException("Not an integer value " + text, 
e);
         }
@@ -229,9 +237,9 @@ implements
         if(input==null) {
             return Optional.empty();
         }
-        val format = getNumberFormat(context);
+        val format = getNumberFormat(context, FormatUsageFor.PARSING);
         format.setParseBigDecimal(true);
-        configureDecimalFormat(context, format);
+
 
         val position = new ParsePosition(0);
         try {
@@ -247,7 +255,7 @@ implements
                     && number.scale()>format.getMaximumFractionDigits()) {
                 throw new TextEntryParseException(String.format(
                         "No more than %d digits can be entered after the 
decimal separator, "
-                        + "got %d in '%s'.", maxFractionDigits, 
number.scale(), input));
+                                + "got %d in '%s'.", maxFractionDigits, 
number.scale(), input));
             }
             return Optional.of(number);
         } catch (final NumberFormatException | ParseException e) {
@@ -257,10 +265,18 @@ implements
         }
     }
 
+    protected static enum FormatUsageFor {
+        PARSING,
+        RENDERING;
+        public boolean isParsing() { return this==PARSING; }
+        public boolean isRendering() { return this==RENDERING; }
+    }
+
     /**
-     * Typically overridden by BigDecimalValueSemantics to set 
MaximumFractionDigits.
+     * Typically overridden by BigDecimalValueSemantics to set min/max 
fractional digits.
      */
-    protected void configureDecimalFormat(final Context context, final 
DecimalFormat format) {}
+    protected void configureDecimalFormat(
+            final Context context, final DecimalFormat format, final 
FormatUsageFor usedFor) {}
 
     // -- TEMPORAL RENDERING
 
@@ -295,7 +311,7 @@ implements
             final @NonNull TemporalValueSemantics.TemporalCharacteristic 
temporalCharacteristic,
             final @NonNull TemporalValueSemantics.OffsetCharacteristic 
offsetCharacteristic) {
 
-             switch (offsetCharacteristic) {
+        switch (offsetCharacteristic) {
         case LOCAL:
             return Optional.empty();
         case OFFSET:
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/nonscalar/DataTableModel.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/nonscalar/DataTableModel.java
index fcc12236e1..8d3cc82ac1 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/nonscalar/DataTableModel.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/interactions/managed/nonscalar/DataTableModel.java
@@ -126,6 +126,7 @@ implements MultiselectChoices {
             dataElements.getValue().stream()
                 //XXX future extension: filter by searchArgument
                 .filter(this::ignoreHidden)
+                //FIXME[ISIS-3167] entities might be detached
                 .sorted(managedMember.getMetaModel().getElementComparator()
                         .orElseGet(()->(a, b)->0)) // else don't sort (no-op 
comparator for streams)
                 .map(domainObject->new DataRow(this, domainObject))
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/Facets.java 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/Facets.java
index cdb051aae3..c1cec8761e 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/Facets.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/util/Facets.java
@@ -59,6 +59,7 @@ import 
org.apache.isis.core.metamodel.facets.object.value.ValueSerializer;
 import 
org.apache.isis.core.metamodel.facets.objectvalue.daterenderedadjust.DateRenderAdjustFacet;
 import 
org.apache.isis.core.metamodel.facets.objectvalue.digits.MaxFractionalDigitsFacet;
 import 
org.apache.isis.core.metamodel.facets.objectvalue.digits.MaxTotalDigitsFacet;
+import 
org.apache.isis.core.metamodel.facets.objectvalue.digits.MinFractionalDigitsFacet;
 import 
org.apache.isis.core.metamodel.facets.objectvalue.fileaccept.FileAcceptFacet;
 import org.apache.isis.core.metamodel.facets.objectvalue.labelat.LabelAtFacet;
 import org.apache.isis.core.metamodel.facets.objectvalue.maxlen.MaxLengthFacet;
@@ -232,9 +233,18 @@ public final class Facets {
         .orElse("label-left");
     }
 
+    public OptionalInt minFractionalDigits(final FacetHolder facetHolder) {
+        return facetHolder.lookupFacet(MinFractionalDigitsFacet.class)
+        .map(MinFractionalDigitsFacet::getMinFractionalDigits)
+        .filter(digits->digits>-1)
+        .map(OptionalInt::of)
+        .orElseGet(OptionalInt::empty);
+    }
+
     public OptionalInt maxFractionalDigits(final FacetHolder facetHolder) {
         return facetHolder.lookupFacet(MaxFractionalDigitsFacet.class)
         .map(MaxFractionalDigitsFacet::getMaxFractionalDigits)
+        .filter(digits->digits>-1)
         .map(OptionalInt::of)
         .orElseGet(OptionalInt::empty);
     }
diff --git 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/BigDecimalValueSemantics.java
 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/BigDecimalValueSemantics.java
index 1c35f413e2..9c11d05676 100644
--- 
a/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/BigDecimalValueSemantics.java
+++ 
b/core/metamodel/src/main/java/org/apache/isis/core/metamodel/valuesemantics/BigDecimalValueSemantics.java
@@ -37,8 +37,8 @@ import 
org.apache.isis.applib.value.semantics.ValueDecomposition;
 import org.apache.isis.applib.value.semantics.ValueSemanticsAbstract;
 import org.apache.isis.applib.value.semantics.ValueSemanticsProvider;
 import org.apache.isis.commons.collections.Can;
-import 
org.apache.isis.core.metamodel.facets.objectvalue.digits.MaxFractionalDigitsFacet;
 import org.apache.isis.core.metamodel.specloader.SpecificationLoader;
+import org.apache.isis.core.metamodel.util.Facets;
 import org.apache.isis.schema.common.v2.ValueType;
 import org.apache.isis.schema.common.v2.ValueWithTypeDto;
 
@@ -134,7 +134,8 @@ implements
     }
 
     @Override
-    protected void configureDecimalFormat(final Context context, final 
DecimalFormat format) {
+    protected void configureDecimalFormat(
+            final Context context, final DecimalFormat format, final 
FormatUsageFor usedFor) {
         if(context==null) {
             return;
         }
@@ -146,16 +147,26 @@ implements
         }
 
         // evaluate any facets that provide the MaximumFractionDigits
-        feature.lookupFacet(MaxFractionalDigitsFacet.class).stream()
-        .mapToInt(MaxFractionalDigitsFacet::getMaxFractionalDigits)
-        .filter(digits->digits>-1)
-        .forEach(digits-> // cardinality 0 or 1
-            format.setMaximumFractionDigits(digits));
+        Facets.maxFractionalDigits(feature)
+            .ifPresent(format::setMaximumFractionDigits);
+
+        // we skip this when PARSING,
+        // because we want to firstly parse any number value into a BigDecimal,
+        // no matter the minimumFractionDigits, which can always be filled up 
with '0' digits later
+        if(usedFor.isRendering()) {
+            // evaluate any facets that provide the MinimumFractionDigits
+            Facets.minFractionalDigits(feature)
+                .ifPresent(format::setMinimumFractionDigits);
+        }
     }
 
     @Override
     public Can<BigDecimal> getExamples() {
-        return Can.of(new BigDecimal("-63.1"), BigDecimal.ZERO);
+        return Can.of(
+                new BigDecimal("1001"),
+                new BigDecimal("-63.1"),
+                new BigDecimal("0.001"),
+                BigDecimal.ZERO);
     }
 
 }
diff --git 
a/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jdo/JavaMathBigDecimalJdo.java
 
b/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jdo/JavaMathBigDecimalJdo.java
index ea77d104f7..236f7edc81 100644
--- 
a/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jdo/JavaMathBigDecimalJdo.java
+++ 
b/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jdo/JavaMathBigDecimalJdo.java
@@ -33,6 +33,7 @@ import org.apache.isis.applib.annotation.Optionality;
 import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
 import org.apache.isis.applib.annotation.Title;
+import org.apache.isis.applib.annotation.ValueSemantics;
 
 import lombok.Getter;
 import lombok.Setter;
@@ -53,6 +54,7 @@ public class JavaMathBigDecimalJdo                            
              // <
         this.readOnlyProperty = initialValue;
         this.readWriteProperty = initialValue;
         this.withMax2FractionDigits = initialValue;
+        this.withFixed2FractionDigits = initialValue;
     }
 
 //tag::class[]
@@ -75,6 +77,14 @@ public class JavaMathBigDecimalJdo                           
               // <
     @Getter @Setter
     private java.math.BigDecimal withMax2FractionDigits;
 
+    @Property(editing = Editing.ENABLED)
+    @ValueSemantics(minFractionalDigits = 2)
+    @PropertyLayout(fieldSetId = "editable-properties", sequence = "3",
+            describedAs = "has 2 fixed fraction digits (scale=2)")
+    @Column(allowsNull = "false", scale = 2)
+    @Getter @Setter
+    private java.math.BigDecimal withFixed2FractionDigits;
+
     @Property(optionality = Optionality.OPTIONAL)                           // 
<.>
     @PropertyLayout(fieldSetId = "optional-properties", sequence = "1")
     @Column(allowsNull = "true")                                            // 
<.>
diff --git 
a/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jpa/JavaMathBigDecimalJpa.java
 
b/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jpa/JavaMathBigDecimalJpa.java
index ca23e1d33d..b7d3d64c23 100644
--- 
a/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jpa/JavaMathBigDecimalJpa.java
+++ 
b/examples/demo/domain/src/main/java/demoapp/dom/types/javamath/bigdecimals/jpa/JavaMathBigDecimalJpa.java
@@ -34,6 +34,7 @@ import org.apache.isis.applib.annotation.Optionality;
 import org.apache.isis.applib.annotation.Property;
 import org.apache.isis.applib.annotation.PropertyLayout;
 import org.apache.isis.applib.annotation.Title;
+import org.apache.isis.applib.annotation.ValueSemantics;
 import org.apache.isis.persistence.jpa.applib.integration.IsisEntityListener;
 
 import lombok.Getter;
@@ -61,6 +62,7 @@ public class JavaMathBigDecimalJpa                            
               //
         this.readOnlyProperty = initialValue;
         this.readWriteProperty = initialValue;
         this.withMax2FractionDigits = initialValue;
+        this.withFixed2FractionDigits = initialValue;
     }
 
 //tag::class[]
@@ -87,6 +89,14 @@ public class JavaMathBigDecimalJpa                           
                //
     @Getter @Setter
     private java.math.BigDecimal withMax2FractionDigits;
 
+    @Property(editing = Editing.ENABLED)
+    @ValueSemantics(minFractionalDigits = 2)
+    @PropertyLayout(fieldSetId = "editable-properties", sequence = "3",
+            describedAs = "has 2 fixed fraction digits (scale=2)")
+    @Column(nullable = false, scale = 2)
+    @Getter @Setter
+    private java.math.BigDecimal withFixed2FractionDigits;
+
     @Property(optionality = Optionality.OPTIONAL)                           // 
<.>
     @PropertyLayout(fieldSetId = "optional-properties", sequence = "1")
     @Column(nullable = true)                                                // 
<.>
diff --git 
a/regressiontests/stable-value/src/test/java/org/apache/isis/testdomain/value/ValueSemanticsTest.java
 
b/regressiontests/stable-value/src/test/java/org/apache/isis/testdomain/value/ValueSemanticsTest.java
index 60e7d6f32e..0be7e4aa65 100644
--- 
a/regressiontests/stable-value/src/test/java/org/apache/isis/testdomain/value/ValueSemanticsTest.java
+++ 
b/regressiontests/stable-value/src/test/java/org/apache/isis/testdomain/value/ValueSemanticsTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.isis.testdomain.value;
 
+import java.math.BigDecimal;
 import java.time.OffsetDateTime;
 import java.time.OffsetTime;
 import java.util.Locale;
@@ -37,6 +38,11 @@ import org.junit.jupiter.params.provider.MethodSource;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.TestPropertySource;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
 import org.apache.isis.applib.annotation.Where;
 import org.apache.isis.applib.graph.tree.TreeNode;
 import org.apache.isis.applib.locale.UserLocale;
@@ -70,11 +76,6 @@ import 
org.apache.isis.testdomain.value.ValueSemanticsTester.PropertyInteraction
 import org.apache.isis.valuetypes.asciidoc.applib.value.AsciiDoc;
 import org.apache.isis.valuetypes.markdown.applib.value.Markdown;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
 import lombok.val;
 
 @SpringBootTest(
@@ -232,8 +233,51 @@ class ValueSemanticsTest {
                             final ValueSemanticsProvider.Context context,
                             final Parser<T> parser) {
 
+                        example.getParseExpectations()
+                        .forEach(parseExpectation->{
+                            val value = parseExpectation.getValue();
+
+                            if(parseExpectation.getExpectedThrows()!=null) {
+
+                                // test parsing that should throw
+                                parseExpectation.getInputSamples()
+                                .forEach(in->{
+                                    
Assertions.assertThrows(parseExpectation.getExpectedThrows(), ()->{
+                                        
parser.parseTextRepresentation(context, in);
+                                    });
+                                });
+
+                            } else {
+
+                                // test parsing that should not throw
+                                parseExpectation.getInputSamples()
+                                .forEach(in->{
+                                    val parsedValue = 
parser.parseTextRepresentation(context, in);
+
+                                    if(value instanceof BigDecimal) {
+                                        assertNumberEquals((BigDecimal)value, 
(BigDecimal)parsedValue);
+                                    } else {
+                                        assertEquals(value, parsedValue);
+                                    }
+
+                                });
+
+                                // test formatting
+                                assertEquals(
+                                        parseExpectation.getExpectedOutput(),
+                                        
parser.parseableTextRepresentation(context, value));
+
+                            }
+
+                        });
+
+                        if(example.getParseExpectations().isNotEmpty()) {
+                            return; // skip round-trip test
+                        }
+
+                        //TODO eventually all examples should have their 
ParseExpectations, so we can remove
                         // Parser round-trip test
-                        for(val value : example.getExamples()) {
+                        for(val value : example.getParserRoundtripExamples()) {
 
                             val stringified = 
parser.parseableTextRepresentation(context, value);
 
@@ -276,7 +320,14 @@ class ValueSemanticsTest {
                             final ValueSemanticsProvider.Context context,
                             final Renderer<T> renderer) {
 
+                        example.getRenderExpectations()
+                        .forEach(renderExpectation->{
+                            val value = renderExpectation.getValue();
+                            assertEquals(renderExpectation.getTitle(), 
renderer.titlePresentation(context, value));
+                            assertEquals(renderExpectation.getHtml(), 
renderer.htmlPresentation(context, value));
+                        });
                     }
+
                     @Override
                     public void testCommand(
                             final ValueSemanticsProvider.Context context,
@@ -336,6 +387,13 @@ class ValueSemanticsTest {
 
     // -- HELPER
 
+    private static void assertNumberEquals(final BigDecimal a, final 
BigDecimal b) {
+        val maxScale = Math.max(a.scale(), b.scale());
+        assertEquals(
+                a.setScale(maxScale),
+                b.setScale(maxScale));
+    }
+
     private InteractionContext interactionContext() {
         return 
InteractionContext.builder().locale(UserLocale.valueOf(Locale.ENGLISH)).build();
     }
diff --git 
a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExample.java
 
b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExample.java
index ae516979d1..7d00e72ce4 100644
--- 
a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExample.java
+++ 
b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExample.java
@@ -45,6 +45,8 @@ import org.apache.isis.applib.annotation.DomainObject;
 import org.apache.isis.applib.annotation.Nature;
 import org.apache.isis.applib.annotation.Programmatic;
 import org.apache.isis.applib.annotation.Property;
+import org.apache.isis.applib.annotation.ValueSemantics;
+import org.apache.isis.applib.exceptions.recoverable.TextEntryParseException;
 import org.apache.isis.applib.graph.tree.TreeAdapter;
 import org.apache.isis.applib.graph.tree.TreeNode;
 import org.apache.isis.applib.graph.tree.TreeState;
@@ -58,6 +60,7 @@ import 
org.apache.isis.applib.value.NamedWithMimeType.CommonMimeType;
 import org.apache.isis.applib.value.Password;
 import org.apache.isis.applib.value.semantics.ValueSemanticsAbstract;
 import org.apache.isis.commons.collections.Can;
+import org.apache.isis.commons.internal.base._Refs;
 import org.apache.isis.commons.internal.base._Temporals;
 import 
org.apache.isis.core.metamodel.valuesemantics.ApplicationFeatureIdValueSemantics;
 import org.apache.isis.extensions.fullcalendar.applib.value.CalendarEvent;
@@ -67,9 +70,12 @@ import org.apache.isis.schema.cmd.v2.CommandDto;
 import org.apache.isis.schema.common.v2.OidDto;
 import org.apache.isis.schema.ixn.v2.InteractionDto;
 
+import lombok.Builder;
 import lombok.Getter;
 import lombok.Setter;
+import lombok.Singular;
 import lombok.SneakyThrows;
+import lombok.val;
 
 public abstract class ValueTypeExample<T> {
 
@@ -85,9 +91,21 @@ public abstract class ValueTypeExample<T> {
         setValue(value);
     }
 
+    /**
+     * Name of the value-type plus suffix if any, as extracted from the 
implementing example name.
+     */
+    @Programmatic
+    public final String getName() {
+        val nameSuffix = extractSuffix(getClass().getSimpleName())
+                .map(s->"_" + s)
+                .orElse("");
+        val name = String.format("%s%s", getValueType().getName(), nameSuffix);
+        return name;
+    }
+
     @Autowired(required = false) List<ValueSemanticsAbstract<T>> semanticsList;
     @Programmatic
-    public Can<T> getExamples() {
+    public Can<T> getParserRoundtripExamples() {
         return Can.ofCollection(semanticsList)
         .getFirst()
         .map(semantics->semantics.getExamples())
@@ -105,6 +123,47 @@ public abstract class ValueTypeExample<T> {
         return (Class<T>) getValue().getClass();
     }
 
+    // -- PARSING
+
+    @lombok.Value @Builder
+    public static class ParseExpectation<T> {
+        final T value;
+        @Singular
+        final List<String> inputSamples;
+        final String expectedOutput;
+        final Class<? extends Throwable> expectedThrows;
+    }
+
+    public Can<ParseExpectation<T>> getParseExpectations() {
+        System.err.printf("skipping parsing test for %s%n", getName());
+        return Can.empty();
+    }
+
+    // -- RENDERING
+
+    @lombok.Value @Builder
+    public static class RenderExpectation<T> {
+        final T value;
+        final String title;
+        final String html;
+    }
+
+    public Can<RenderExpectation<T>> getRenderExpectations() {
+        System.err.printf("skipping rendering test for %s%n", getName());
+        return Can.empty();
+    }
+
+    // -- HELPER
+
+    private static Optional<String> extractSuffix(final String name) {
+        if(!name.contains("_")) {
+            return Optional.empty();
+        }
+        val ref = _Refs.stringRef(name);
+        ref.cutAtIndexOfAndDrop("_");
+        return Optional.of(ref.getValue());
+    }
+
     // -- EXAMPLES - BASIC
 
     @Named("isis.testdomain.valuetypes.ValueTypeExampleBoolean")
@@ -302,7 +361,7 @@ public abstract class ValueTypeExample<T> {
 
         //FIXME does not handle example Float.MIN_VALUE well
         @Deprecated // remove override once fixed
-        @Override public Can<Float> getExamples() {
+        @Override public Can<Float> getParserRoundtripExamples() {
             return Can.of(value, updateValue);
         }
     }
@@ -319,11 +378,13 @@ public abstract class ValueTypeExample<T> {
 
         //FIXME does not handle example Double.MIN_VALUE well
         @Deprecated // remove override once fixed
-        @Override public Can<Double> getExamples() {
+        @Override public Can<Double> getParserRoundtripExamples() {
             return Can.of(value, updateValue);
         }
     }
 
+    // -- BIG INTEGER
+
     @Named("isis.testdomain.valuetypes.ValueTypeExampleBigInteger")
     @DomainObject(
             nature = Nature.BEAN)
@@ -335,15 +396,72 @@ public abstract class ValueTypeExample<T> {
         private BigInteger updateValue = BigInteger.ZERO;
     }
 
-    @Named("isis.testdomain.valuetypes.ValueTypeExampleBigDecimal")
+    // -- BIG DECIMAL
+
+    @Named("isis.testdomain.valuetypes.ValueTypeExampleBigDecimal_default")
     @DomainObject(
             nature = Nature.BEAN)
-    public static class ValueTypeExampleBigDecimal
+    public static class ValueTypeExampleBigDecimal_default
     extends ValueTypeExample<BigDecimal> {
         @Property @Getter @Setter
-        private BigDecimal value = new BigDecimal("-63.1");
+        private BigDecimal value = new BigDecimal("-63.123456");
+        @Getter
+        private BigDecimal updateValue = BigDecimal.ZERO;
+    }
+
+    
@Named("isis.testdomain.valuetypes.ValueTypeExampleBigDecimal_fixedFractionalDigits")
+    @DomainObject(
+            nature = Nature.BEAN)
+    public static class ValueTypeExampleBigDecimal_fixedFractionalDigits
+    extends ValueTypeExample<BigDecimal> {
+        @Property @ValueSemantics(minFractionalDigits = 2, maxFractionalDigits 
= 2)
+        @Getter @Setter
+        private BigDecimal value = new BigDecimal("-63.12");
         @Getter
         private BigDecimal updateValue = BigDecimal.ZERO;
+
+        // with this example maxFractionalDigits = 2 must not be exceeded
+        @Override public Can<BigDecimal> getParserRoundtripExamples() {
+            return Can.of(value, updateValue, new BigDecimal("0.1"));
+        }
+
+        @Override
+        public Can<ParseExpectation<BigDecimal>> getParseExpectations() {
+            return Can.of(
+                    ParseExpectation.<BigDecimal>builder()
+                        .value(new BigDecimal("123"))
+                        .inputSample("123")
+                        .inputSample("123.0")
+                        .inputSample("123.00")
+                        .expectedOutput("123.00")
+                        .build(),
+                    ParseExpectation.<BigDecimal>builder()
+                        .value(new BigDecimal("123.45"))
+                        .inputSample("123.45")
+                        .expectedOutput("123.45")
+                        .build(),
+                    ParseExpectation.<BigDecimal>builder()
+                        .value(new BigDecimal("123.45"))
+                        .inputSample("123.456")
+                        
//org.apache.isis.applib.exceptions.recoverable.TextEntryParseException:
+                        // No more than 2 digits can be entered after the 
decimal separator, got 3 in '123.456'.
+                        .expectedThrows(TextEntryParseException.class)
+                        .build()
+                );
+        }
+
+        @Override
+        public Can<RenderExpectation<BigDecimal>> getRenderExpectations() {
+            return Can.of(
+                    RenderExpectation.<BigDecimal>builder()
+                        .value(new 
BigDecimal("123")).title("123.00").html("123.00").build(),
+                    RenderExpectation.<BigDecimal>builder()
+                        .value(new 
BigDecimal("0")).title("0.00").html("0.00").build(),
+                    RenderExpectation.<BigDecimal>builder()
+                        .value(new 
BigDecimal("123.456")).title("123.46").html("123.46").build()
+                );
+        }
+
     }
 
     // -- EXAMPLES - TEMPORAL - LEGACY
diff --git 
a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExampleService.java
 
b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExampleService.java
index 7747b4f5f3..87d9e08081 100644
--- 
a/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExampleService.java
+++ 
b/regressiontests/stable/src/main/java/org/apache/isis/testdomain/model/valuetypes/ValueTypeExampleService.java
@@ -43,7 +43,7 @@ public class ValueTypeExampleService {
     @Value(staticConstructor = "of")
     public static class Scenario implements Comparable<Scenario> {
         static Scenario of(final ValueTypeExample<?> example) {
-            val name = String.format("%s", example.getValueType().getName());
+            val name = example.getName();
             return Scenario.of(name, Arguments.of(
                     name,
                     example.getValueType(),

Reply via email to