mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Add ValueRangeBehavior - e.g., for min/max numeric value
This commit is contained in:
@ -780,7 +780,7 @@ public class QInstanceValidator
|
||||
{
|
||||
if(assertCondition(StringUtils.hasContent(association.getName()), "missing a name for an Association on table " + table.getName()))
|
||||
{
|
||||
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
|
||||
String messageSuffix = " for Association " + association.getName() + " on table " + table.getName();
|
||||
boolean recognizedTable = false;
|
||||
if(assertCondition(StringUtils.hasContent(association.getAssociatedTableName()), "missing associatedTableName" + messageSuffix))
|
||||
{
|
||||
@ -988,7 +988,15 @@ public class QInstanceValidator
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<FieldBehavior<?>> behaviorClass = (Class<FieldBehavior<?>>) fieldBehavior.getClass();
|
||||
|
||||
errors.addAll(fieldBehavior.validateBehaviorConfiguration(table, field));
|
||||
List<String> behaviorErrors = fieldBehavior.validateBehaviorConfiguration(table, field);
|
||||
if(behaviorErrors != null)
|
||||
{
|
||||
String prefixMinusTrailingSpace = prefix.replaceFirst(" *$", "");
|
||||
for(String behaviorError : behaviorErrors)
|
||||
{
|
||||
errors.add(prefixMinusTrailingSpace + ": " + behaviorClass.getSimpleName() + ": " + behaviorError);
|
||||
}
|
||||
}
|
||||
|
||||
if(!fieldBehavior.allowMultipleBehaviorsOfThisType())
|
||||
{
|
||||
|
@ -0,0 +1,477 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.fields;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Validate the min & max value for numeric fields.
|
||||
**
|
||||
** For each min & max, there are 4 possible settings:
|
||||
** - value - the number that is compared.
|
||||
** - allowEqualTo - defaults to true. controls if < (>) or ≤ (≥)
|
||||
** - behavior - defaults to ERROR. optionally can be "CLIP" instead.
|
||||
** - clipAmount - if clipping, and not allowing equalTo, how much off the limit
|
||||
** value should be added or subtracted. Defaults to 1.
|
||||
**
|
||||
** Convenient `withMin()` and `withMax()` methods exist for setting all 4
|
||||
** properties for each of min or max. Else, fluent-setters are recommended.
|
||||
*******************************************************************************/
|
||||
public class ValueRangeBehavior implements FieldBehavior<ValueRangeBehavior>
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public enum Behavior
|
||||
{
|
||||
ERROR,
|
||||
CLIP
|
||||
}
|
||||
|
||||
|
||||
|
||||
private Number minValue;
|
||||
private boolean minAllowEqualTo = true;
|
||||
private Behavior minBehavior = Behavior.ERROR;
|
||||
private BigDecimal minClipAmount = BigDecimal.ONE;
|
||||
|
||||
private Number maxValue;
|
||||
private boolean maxAllowEqualTo = true;
|
||||
private Behavior maxBehavior = Behavior.ERROR;
|
||||
private BigDecimal maxClipAmount = BigDecimal.ONE;
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Constructor
|
||||
**
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior()
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public ValueRangeBehavior getDefault()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public void apply(ValueBehaviorApplier.Action action, List<QRecord> recordList, QInstance instance, QTableMetaData table, QFieldMetaData field)
|
||||
{
|
||||
BigDecimal minLimitBigDecimal = minValue == null ? null : new BigDecimal(minValue.toString());
|
||||
String minLimitString = minValue == null ? null : minValue.toString();
|
||||
|
||||
BigDecimal maxLimitBigDecimal = maxValue == null ? null : new BigDecimal(maxValue.toString());
|
||||
String maxLimitString = maxValue == null ? null : maxValue.toString();
|
||||
|
||||
for(QRecord record : recordList)
|
||||
{
|
||||
BigDecimal recordValue = record.getValueBigDecimal(field.getName());
|
||||
if(recordValue != null)
|
||||
{
|
||||
if(minLimitBigDecimal != null)
|
||||
{
|
||||
int compare = recordValue.compareTo(minLimitBigDecimal);
|
||||
if(compare < 0 || (compare == 0 && !minAllowEqualTo))
|
||||
{
|
||||
if(this.minBehavior == Behavior.ERROR)
|
||||
{
|
||||
String operator = minAllowEqualTo ? "" : "greater than ";
|
||||
record.addError(new BadInputStatusMessage("The value for " + field.getLabel() + " is too small (minimum allowed value is " + operator + minLimitString + ")"));
|
||||
}
|
||||
else if(this.minBehavior == Behavior.CLIP)
|
||||
{
|
||||
if(minAllowEqualTo)
|
||||
{
|
||||
record.setValue(field.getName(), minLimitBigDecimal);
|
||||
}
|
||||
else
|
||||
{
|
||||
record.setValue(field.getName(), minLimitBigDecimal.add(minClipAmount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(maxLimitBigDecimal != null)
|
||||
{
|
||||
int compare = recordValue.compareTo(maxLimitBigDecimal);
|
||||
if(compare > 0 || (compare == 0 && !maxAllowEqualTo))
|
||||
{
|
||||
if(this.maxBehavior == Behavior.ERROR)
|
||||
{
|
||||
String operator = maxAllowEqualTo ? "" : "less than ";
|
||||
record.addError(new BadInputStatusMessage("The value for " + field.getLabel() + " is too large (maximum allowed value is " + operator + maxLimitString + ")"));
|
||||
}
|
||||
else if(this.maxBehavior == Behavior.CLIP)
|
||||
{
|
||||
if(maxAllowEqualTo)
|
||||
{
|
||||
record.setValue(field.getName(), maxLimitBigDecimal);
|
||||
}
|
||||
else
|
||||
{
|
||||
record.setValue(field.getName(), maxLimitBigDecimal.subtract(maxClipAmount));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public boolean allowMultipleBehaviorsOfThisType()
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
@Override
|
||||
public List<String> validateBehaviorConfiguration(QTableMetaData tableMetaData, QFieldMetaData fieldMetaData)
|
||||
{
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if(minValue == null && maxValue == null)
|
||||
{
|
||||
errors.add("Either minValue or maxValue (or both) must be set.");
|
||||
}
|
||||
|
||||
if(minValue != null && maxValue != null && new BigDecimal(minValue.toString()).compareTo(new BigDecimal(maxValue.toString())) > 0)
|
||||
{
|
||||
errors.add("minValue must be >= maxValue.");
|
||||
}
|
||||
|
||||
if(fieldMetaData != null && fieldMetaData.getType() != null && !fieldMetaData.getType().isNumeric())
|
||||
{
|
||||
errors.add("can only be applied to a numeric type field.");
|
||||
}
|
||||
|
||||
return (errors);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public ValueRangeBehavior withMin(Number value, boolean allowEqualTo, Behavior behavior, BigDecimal clipAmount)
|
||||
{
|
||||
setMinValue(value);
|
||||
setMinAllowEqualTo(allowEqualTo);
|
||||
setMinBehavior(behavior);
|
||||
setMinClipAmount(clipAmount);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public ValueRangeBehavior withMax(Number value, boolean allowEqualTo, Behavior behavior, BigDecimal clipAmount)
|
||||
{
|
||||
setMaxValue(value);
|
||||
setMaxAllowEqualTo(allowEqualTo);
|
||||
setMaxBehavior(behavior);
|
||||
setMaxClipAmount(clipAmount);
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for minValue
|
||||
*******************************************************************************/
|
||||
public Number getMinValue()
|
||||
{
|
||||
return (this.minValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for minValue
|
||||
*******************************************************************************/
|
||||
public void setMinValue(Number minValue)
|
||||
{
|
||||
this.minValue = minValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for minValue
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMinValue(Number minValue)
|
||||
{
|
||||
this.minValue = minValue;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxValue
|
||||
*******************************************************************************/
|
||||
public Number getMaxValue()
|
||||
{
|
||||
return (this.maxValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for maxValue
|
||||
*******************************************************************************/
|
||||
public void setMaxValue(Number maxValue)
|
||||
{
|
||||
this.maxValue = maxValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for maxValue
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMaxValue(Number maxValue)
|
||||
{
|
||||
this.maxValue = maxValue;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for minAllowEqualTo
|
||||
*******************************************************************************/
|
||||
public boolean getMinAllowEqualTo()
|
||||
{
|
||||
return (this.minAllowEqualTo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for minAllowEqualTo
|
||||
*******************************************************************************/
|
||||
public void setMinAllowEqualTo(boolean minAllowEqualTo)
|
||||
{
|
||||
this.minAllowEqualTo = minAllowEqualTo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for minAllowEqualTo
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMinAllowEqualTo(boolean minAllowEqualTo)
|
||||
{
|
||||
this.minAllowEqualTo = minAllowEqualTo;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxAllowEqualTo
|
||||
*******************************************************************************/
|
||||
public boolean getMaxAllowEqualTo()
|
||||
{
|
||||
return (this.maxAllowEqualTo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for maxAllowEqualTo
|
||||
*******************************************************************************/
|
||||
public void setMaxAllowEqualTo(boolean maxAllowEqualTo)
|
||||
{
|
||||
this.maxAllowEqualTo = maxAllowEqualTo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for maxAllowEqualTo
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMaxAllowEqualTo(boolean maxAllowEqualTo)
|
||||
{
|
||||
this.maxAllowEqualTo = maxAllowEqualTo;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for minBehavior
|
||||
*******************************************************************************/
|
||||
public Behavior getMinBehavior()
|
||||
{
|
||||
return (this.minBehavior);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for minBehavior
|
||||
*******************************************************************************/
|
||||
public void setMinBehavior(Behavior minBehavior)
|
||||
{
|
||||
this.minBehavior = minBehavior;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for minBehavior
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMinBehavior(Behavior minBehavior)
|
||||
{
|
||||
this.minBehavior = minBehavior;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxBehavior
|
||||
*******************************************************************************/
|
||||
public Behavior getMaxBehavior()
|
||||
{
|
||||
return (this.maxBehavior);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for maxBehavior
|
||||
*******************************************************************************/
|
||||
public void setMaxBehavior(Behavior maxBehavior)
|
||||
{
|
||||
this.maxBehavior = maxBehavior;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for maxBehavior
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMaxBehavior(Behavior maxBehavior)
|
||||
{
|
||||
this.maxBehavior = maxBehavior;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for minClipAmount
|
||||
*******************************************************************************/
|
||||
public BigDecimal getMinClipAmount()
|
||||
{
|
||||
return (this.minClipAmount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for minClipAmount
|
||||
*******************************************************************************/
|
||||
public void setMinClipAmount(BigDecimal minClipAmount)
|
||||
{
|
||||
this.minClipAmount = minClipAmount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for minClipAmount
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMinClipAmount(BigDecimal minClipAmount)
|
||||
{
|
||||
this.minClipAmount = minClipAmount;
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Getter for maxClipAmount
|
||||
*******************************************************************************/
|
||||
public BigDecimal getMaxClipAmount()
|
||||
{
|
||||
return (this.maxClipAmount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Setter for maxClipAmount
|
||||
*******************************************************************************/
|
||||
public void setMaxClipAmount(BigDecimal maxClipAmount)
|
||||
{
|
||||
this.maxClipAmount = maxClipAmount;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Fluent setter for maxClipAmount
|
||||
*******************************************************************************/
|
||||
public ValueRangeBehavior withMaxClipAmount(BigDecimal maxClipAmount)
|
||||
{
|
||||
this.maxClipAmount = maxClipAmount;
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
@ -64,6 +64,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.DateTimeDisplayValue
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueRangeBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
|
||||
@ -1926,6 +1927,20 @@ public class QInstanceValidatorTest extends BaseTest
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testFieldBehaviorsWithTheirOwnValidateMethods()
|
||||
{
|
||||
Function<QInstance, QFieldMetaData> fieldExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("id");
|
||||
assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance).withBehavior(new ValueRangeBehavior())),
|
||||
"Field id in table person: ValueRangeBehavior: Either minValue or maxValue (or both) must be set.");
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.model.metadata.fields;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||
import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueRangeBehavior.Behavior;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionAssert;
|
||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Unit test for ValueOutsideOfRangeBehavior
|
||||
*******************************************************************************/
|
||||
class ValueRangeBehaviorTest extends BaseTest
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void test()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
|
||||
table.getField("noOfShoes").withBehavior(new ValueRangeBehavior().withMinValue(0));
|
||||
table.getField("cost").withBehavior(new ValueRangeBehavior().withMaxValue(new BigDecimal("3.50")));
|
||||
table.getField("price").withBehavior(new ValueRangeBehavior()
|
||||
.withMin(BigDecimal.ZERO, false, Behavior.CLIP, new BigDecimal(".01"))
|
||||
.withMaxValue(new BigDecimal(100)).withMaxAllowEqualTo(false));
|
||||
|
||||
List<QRecord> recordList = List.of(
|
||||
new QRecord().withValue("id", 1).withValue("noOfShoes", -1).withValue("cost", new BigDecimal("3.50")).withValue("price", new BigDecimal(-1)),
|
||||
new QRecord().withValue("id", 2).withValue("noOfShoes", 0).withValue("cost", new BigDecimal("3.51")).withValue("price", new BigDecimal(200)),
|
||||
new QRecord().withValue("id", 3).withValue("noOfShoes", 1).withValue("cost", new BigDecimal("3.50")).withValue("price", new BigDecimal("99.99"))
|
||||
);
|
||||
ValueBehaviorApplier.applyFieldBehaviors(ValueBehaviorApplier.Action.INSERT, qInstance, table, recordList, null);
|
||||
|
||||
{
|
||||
QRecord record = getRecordById(recordList, 1);
|
||||
assertEquals(-1, record.getValueInteger("noOfShoes")); // error (but didn't change value)
|
||||
assertEquals(new BigDecimal("3.50"), record.getValueBigDecimal("cost")); // all okay
|
||||
assertEquals(new BigDecimal("0.01"), record.getValueBigDecimal("price")); // got clipped
|
||||
assertThat(record.getErrors())
|
||||
.hasSize(1)
|
||||
.anyMatch(e -> e.getMessage().equals("The value for No Of Shoes is too small (minimum allowed value is 0)"));
|
||||
}
|
||||
|
||||
{
|
||||
QRecord record = getRecordById(recordList, 2);
|
||||
assertEquals(0, record.getValueInteger("noOfShoes")); // all ok
|
||||
assertEquals(new BigDecimal("3.51"), record.getValueBigDecimal("cost")); // error (but didn't change value)
|
||||
assertEquals(new BigDecimal(200), record.getValueBigDecimal("price")); // error (but didn't change value)
|
||||
assertThat(record.getErrors())
|
||||
.hasSize(2)
|
||||
.anyMatch(e -> e.getMessage().equals("The value for Cost is too large (maximum allowed value is 3.50)"))
|
||||
.anyMatch(e -> e.getMessage().equals("The value for Price is too large (maximum allowed value is less than 100)"));
|
||||
}
|
||||
|
||||
{
|
||||
QRecord record = getRecordById(recordList, 3);
|
||||
assertEquals(1, record.getValueInteger("noOfShoes")); // all ok
|
||||
assertEquals(new BigDecimal("3.50"), record.getValueBigDecimal("cost")); // all ok
|
||||
assertEquals(new BigDecimal("99.99"), record.getValueBigDecimal("price")); // all ok
|
||||
assertThat(record.getErrors()).isNullOrEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testValidation()
|
||||
{
|
||||
QInstance qInstance = QContext.getQInstance();
|
||||
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||
QFieldMetaData noOfShoesField = table.getField("noOfShoes");
|
||||
QFieldMetaData firstNameField = table.getField("firstName");
|
||||
|
||||
CollectionAssert.assertThat(new ValueRangeBehavior().validateBehaviorConfiguration(table, noOfShoesField))
|
||||
.matchesAll(List.of("Either minValue or maxValue (or both) must be set."), Objects::equals);
|
||||
|
||||
CollectionAssert.assertThat(new ValueRangeBehavior().withMinValue(0).validateBehaviorConfiguration(table, noOfShoesField)).isNullOrEmpty();
|
||||
CollectionAssert.assertThat(new ValueRangeBehavior().withMaxValue(100).validateBehaviorConfiguration(table, noOfShoesField)).isNullOrEmpty();
|
||||
CollectionAssert.assertThat(new ValueRangeBehavior().withMinValue(0).withMaxValue(100).validateBehaviorConfiguration(table, noOfShoesField)).isNullOrEmpty();
|
||||
|
||||
CollectionAssert.assertThat(new ValueRangeBehavior().withMinValue(1).withMaxValue(0).validateBehaviorConfiguration(table, noOfShoesField))
|
||||
.matchesAll(List.of("minValue must be >= maxValue."), Objects::equals);
|
||||
|
||||
CollectionAssert.assertThat(new ValueRangeBehavior().withMinValue(1).validateBehaviorConfiguration(table, firstNameField))
|
||||
.matchesAll(List.of("can only be applied to a numeric type field."), Objects::equals);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private static QRecord getRecordById(List<QRecord> recordList, Integer id)
|
||||
{
|
||||
Optional<QRecord> recordOpt = recordList.stream().filter(r -> r.getValueInteger("id").equals(id)).findFirst();
|
||||
if(recordOpt.isEmpty())
|
||||
{
|
||||
fail("Didn't find record with id=" + id);
|
||||
}
|
||||
return (recordOpt.get());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,184 @@
|
||||
/*
|
||||
* QQQ - Low-code Application Framework for Engineers.
|
||||
* Copyright (C) 2021-2025. Kingsrook, LLC
|
||||
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
|
||||
* contact@kingsrook.com
|
||||
* https://github.com/Kingsrook/
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.kingsrook.qqq.backend.core.utils;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** assertions against collections
|
||||
*******************************************************************************/
|
||||
public class CollectionAssert<A> extends AbstractAssert<CollectionAssert<A>, Collection<A>>
|
||||
{
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
protected CollectionAssert(Collection<A> actual, Class<?> selfType)
|
||||
{
|
||||
super(actual, selfType);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
**
|
||||
***************************************************************************/
|
||||
public static <A> CollectionAssert<A> assertThat(Collection<A> actualCollection)
|
||||
{
|
||||
return (new CollectionAssert<>(actualCollection, CollectionAssert.class));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public <E> CollectionAssert<A> matchesAllowingExpectedToHaveMore(Collection<E> expected, BiFunction<E, A, Boolean> predicate)
|
||||
{
|
||||
if(actual == null && expected != null)
|
||||
{
|
||||
fail("Actual collection was null, but expected collection was not-null");
|
||||
return (this);
|
||||
}
|
||||
else if(actual != null && expected == null)
|
||||
{
|
||||
fail("Actual collection was not null, but expected collection was null");
|
||||
return (this);
|
||||
}
|
||||
else if(actual == null && expected == null)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
assertTrue(actual.size() >= expected.size(), "Actual collection size [" + actual.size() + "] should be >= expected collection size [" + expected.size() + "]");
|
||||
|
||||
matchElements(expected, predicate);
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
private <E> void matchElements(Collection<E> expected, BiFunction<E, A, Boolean> predicate)
|
||||
{
|
||||
List<Integer> nonMatchingExpectedIndexes = new ArrayList<>();
|
||||
Set<Integer> matchedActualIndexes = new HashSet<>();
|
||||
|
||||
List<E> expectedList = new ArrayList<>(expected);
|
||||
List<A> actualList = new ArrayList<>(actual);
|
||||
|
||||
for(int eIndex = 0; eIndex < expectedList.size(); eIndex++)
|
||||
{
|
||||
E e = expectedList.get(eIndex);
|
||||
|
||||
boolean matchedThieE = false;
|
||||
|
||||
for(int aIndex = 0; aIndex < actualList.size(); aIndex++)
|
||||
{
|
||||
A a = actualList.get(aIndex);
|
||||
if(!matchedThieE && !matchedActualIndexes.contains(aIndex)) // don't re-check an already-matched item
|
||||
{
|
||||
if(predicate.apply(e, a))
|
||||
{
|
||||
matchedActualIndexes.add(aIndex);
|
||||
matchedThieE = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!matchedThieE)
|
||||
{
|
||||
nonMatchingExpectedIndexes.add(eIndex);
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue(nonMatchingExpectedIndexes.isEmpty(), "Did not find a match for indexes " + nonMatchingExpectedIndexes + "\n from expected collection: " + expected + "\n in actual collection: " + actual);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public <E> CollectionAssert<A> matchesAll(Collection<E> expected, BiFunction<E, A, Boolean> predicate)
|
||||
{
|
||||
if(actual == null && expected != null)
|
||||
{
|
||||
fail("Actual collection was null, but expected collection was not-null");
|
||||
return (this);
|
||||
}
|
||||
else if(actual != null && expected == null)
|
||||
{
|
||||
fail("Actual collection was not null, but expected collection was null");
|
||||
return (this);
|
||||
}
|
||||
else if(actual == null && expected == null)
|
||||
{
|
||||
return (this);
|
||||
}
|
||||
|
||||
assertEquals(expected.size(), actual.size(), "Expected size of collections");
|
||||
|
||||
matchElements(expected, predicate);
|
||||
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public CollectionAssert<A> isNullOrEmpty()
|
||||
{
|
||||
if(actual != null)
|
||||
{
|
||||
assertEquals(0, actual.size(), "Expected collection to be null or empty");
|
||||
}
|
||||
return (this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/***************************************************************************
|
||||
*
|
||||
***************************************************************************/
|
||||
public CollectionAssert<A> isEmpty()
|
||||
{
|
||||
assertEquals(0, actual.size(), "Expected collection to be empty");
|
||||
return (this);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user