diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/Form.java b/core/src/main/java/com/cosium/hal_mock_mvc/Form.java new file mode 100644 index 0000000..5e037ad --- /dev/null +++ b/core/src/main/java/com/cosium/hal_mock_mvc/Form.java @@ -0,0 +1,183 @@ +package com.cosium.hal_mock_mvc; + +import static java.util.Objects.requireNonNull; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.ResultActions; + +/** + * @author Réda Housni Alaoui + */ +public class Form { + + private final RequestExecutor requestExecutor; + private final ObjectMapper objectMapper; + private final Template template; + + private final Map> propertyByName = new HashMap<>(); + + Form(RequestExecutor requestExecutor, ObjectMapper objectMapper, Template template) { + this.requestExecutor = requireNonNull(requestExecutor); + this.objectMapper = requireNonNull(objectMapper); + this.template = requireNonNull(template); + } + + public Form withString(String propertyName, String value) throws Exception { + FormProperty property = + new FormProperty<>( + String.class, propertyName, Optional.ofNullable(value).stream().toList(), false); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withBoolean(String propertyName, Boolean value) throws Exception { + FormProperty property = + new FormProperty<>( + Boolean.class, propertyName, Optional.ofNullable(value).stream().toList(), false); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withInteger(String propertyName, Integer value) throws Exception { + FormProperty property = + new FormProperty<>( + Integer.class, propertyName, Optional.ofNullable(value).stream().toList(), false); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withLong(String propertyName, Long value) throws Exception { + FormProperty property = + new FormProperty<>( + Long.class, propertyName, Optional.ofNullable(value).stream().toList(), false); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withDouble(String propertyName, Double value) throws Exception { + FormProperty property = + new FormProperty<>( + Double.class, propertyName, Optional.ofNullable(value).stream().toList(), false); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withStrings(String propertyName, List value) throws Exception { + FormProperty property = new FormProperty<>(String.class, propertyName, value, true); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withBooleans(String propertyName, List value) throws Exception { + FormProperty property = new FormProperty<>(Boolean.class, propertyName, value, true); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withIntegers(String propertyName, List value) throws Exception { + FormProperty property = new FormProperty<>(Integer.class, propertyName, value, true); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withLongs(String propertyName, List value) throws Exception { + FormProperty property = new FormProperty<>(Long.class, propertyName, value, true); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public Form withDoubles(String propertyName, List value) throws Exception { + FormProperty property = new FormProperty<>(Double.class, propertyName, value, true); + propertyByName.put(property.name(), validate(property)); + return this; + } + + public ResultActions submit() throws Exception { + String contentType = template.representation().contentType(); + if (!MediaType.APPLICATION_JSON_VALUE.equals(contentType)) { + throw new UnsupportedOperationException( + "Expected content type is '%s'. For now, the only supported content type is '%s'." + .formatted(contentType, MediaType.APPLICATION_JSON_VALUE)); + } + + List templateProperties = + template.representation().propertyByName().values().stream() + .map( + propertyRepresentation -> + new TemplateProperty(requestExecutor, objectMapper, propertyRepresentation)) + .toList(); + + Map payload = new HashMap<>(); + + templateProperties.forEach( + property -> { + Object defaultValue = property.defaultValue().orElse(null); + if (defaultValue == null) { + return; + } + payload.put(property.name(), defaultValue); + }); + + List expectedBadRequestReasons = new ArrayList<>(); + + propertyByName + .values() + .forEach( + formProperty -> + formProperty + .populateRequestPayload(payload) + .serverSideVerifiableErrorMessage() + .ifPresent(expectedBadRequestReasons::add)); + + templateProperties.stream() + .filter(TemplateProperty::isRequired) + .map(TemplateProperty::name) + .filter(propertyName -> payload.get(propertyName) == null) + .findFirst() + .map("Property '%s' is required but is missing"::formatted) + .ifPresent(expectedBadRequestReasons::add); + + ResultActions resultActions = template.submit(objectMapper.writeValueAsString(payload)); + if (expectedBadRequestReasons.isEmpty()) { + return resultActions; + } + int status = resultActions.andReturn().getResponse().getStatus(); + HttpStatus.Series statusSeries = HttpStatus.Series.resolve(status); + if (statusSeries == HttpStatus.Series.CLIENT_ERROR) { + return resultActions; + } + throw new AssertionError( + "An http status code 400 was expected because of the following reasons: [%s]. Got http status code %s instead." + .formatted(String.join(",", expectedBadRequestReasons), status)); + } + + private ValidatedFormProperty validate(FormProperty property) throws Exception { + TemplatePropertyRepresentation representation = requireTemplate(property); + if (representation.readOnly()) { + throw new AssertionError( + "Cannot set value for read-only property '%s'".formatted(property.name())); + } + ValidatedFormProperty validatedFormProperty = + new TemplateProperty(requestExecutor, objectMapper, representation).validate(property); + ValidatedFormProperty.ValidationError firstValidationError = + validatedFormProperty.firstValidationError(); + if (firstValidationError != null && !firstValidationError.serverSideVerifiable()) { + throw new AssertionError(firstValidationError.reason()); + } + return validatedFormProperty; + } + + private TemplatePropertyRepresentation requireTemplate(FormProperty property) { + TemplateRepresentation templateRepresentation = template.representation(); + return Optional.ofNullable(templateRepresentation.propertyByName().get(property.name())) + .orElseThrow( + () -> new AssertionError("No property '%s' found.".formatted(property.name()))); + } +} diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/FormProperty.java b/core/src/main/java/com/cosium/hal_mock_mvc/FormProperty.java new file mode 100644 index 0000000..24b0c4a --- /dev/null +++ b/core/src/main/java/com/cosium/hal_mock_mvc/FormProperty.java @@ -0,0 +1,70 @@ +package com.cosium.hal_mock_mvc; + +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * @author Réda Housni Alaoui + */ +record FormProperty(Class valueType, String name, List values, boolean array) { + + FormProperty { + if (!array && values.size() > 1) { + throw new IllegalArgumentException("Non array property can't hold more than 1 value."); + } + requireNonNull(valueType); + requireNonNull(name); + values = values.stream().filter(Objects::nonNull).toList(); + } + + public void populateRequestPayload(Map requestPayload) { + Object value; + if (array) { + value = values; + } else { + value = values.stream().findFirst().orElse(null); + } + requestPayload.put(name, value); + } + + public boolean isNumberValueType() { + return Set.of(Integer.class, Long.class, Double.class).contains(valueType); + } + + public List toDoubleValues() { + if (!isNumberValueType()) { + throw new IllegalArgumentException("%s is not a number type".formatted(valueType)); + } + if (Integer.class.equals(valueType)) { + return values.stream() + .map(Integer.class::cast) + .map( + integer -> { + if (integer == null) { + return null; + } + return integer.doubleValue(); + }) + .toList(); + } else if (Long.class.equals(valueType)) { + return values.stream() + .map(Long.class::cast) + .map( + aLong -> { + if (aLong == null) { + return null; + } + return aLong.doubleValue(); + }) + .toList(); + } else if (Double.class.equals(valueType)) { + return values.stream().map(Double.class::cast).toList(); + } else { + throw new IllegalArgumentException("Unexpected value type %s".formatted(valueType)); + } + } +} diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/Template.java b/core/src/main/java/com/cosium/hal_mock_mvc/Template.java index 4743c2a..34fbd33 100644 --- a/core/src/main/java/com/cosium/hal_mock_mvc/Template.java +++ b/core/src/main/java/com/cosium/hal_mock_mvc/Template.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import com.fasterxml.jackson.databind.ObjectMapper; import java.net.URI; import org.springframework.test.web.servlet.ResultActions; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; @@ -13,6 +14,7 @@ public class Template implements SubmittableTemplate { private final RequestExecutor requestExecutor; + private final ObjectMapper objectMapper; private final String key; private final TemplateRepresentation representation; @@ -21,10 +23,12 @@ public class Template implements SubmittableTemplate { Template( RequestExecutor requestExecutor, + ObjectMapper objectMapper, String baseUri, String key, TemplateRepresentation representation) { this.requestExecutor = requireNonNull(requestExecutor); + this.objectMapper = requireNonNull(objectMapper); this.key = requireNonNull(key); this.representation = requireNonNull(representation); @@ -32,6 +36,10 @@ public class Template implements SubmittableTemplate { target = URI.create(representation.target().orElse(baseUri)); } + public Form createForm() { + return new Form(requestExecutor, objectMapper, this); + } + @Override public HalMockMvc createAndShift() throws Exception { return createAndShift(null); diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptions.java b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptions.java new file mode 100644 index 0000000..d5f424b --- /dev/null +++ b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptions.java @@ -0,0 +1,106 @@ +package com.cosium.hal_mock_mvc; + +import static java.util.Objects.requireNonNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.cosium.hal_mock_mvc.template.options.InlineElementRepresentation; +import com.cosium.hal_mock_mvc.template.options.OptionsLinkRepresentation; +import com.cosium.hal_mock_mvc.template.options.OptionsRepresentation; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import org.springframework.hateoas.Link; + +/** + * @author Réda Housni Alaoui + */ +class TemplateOptions { + + private final RequestExecutor requestExecutor; + private final ObjectMapper objectMapper; + private final OptionsRepresentation representation; + + TemplateOptions( + RequestExecutor requestExecutor, + ObjectMapper objectMapper, + OptionsRepresentation representation) { + this.requestExecutor = requireNonNull(requestExecutor); + this.objectMapper = requireNonNull(objectMapper); + this.representation = requireNonNull(representation); + } + + public ValidatedFormProperty validate(FormProperty property) throws Exception { + if (!String.class.equals(property.valueType())) { + + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value of type '%s' is not valid because the type should be of type 'String' given property '%s' expects a value from an enumeration of options." + .formatted(property.valueType(), property.name())); + } + + long numberOfValues = property.values().size(); + + Long maxItems = representation.maxItems().orElse(null); + if (maxItems != null && numberOfValues > maxItems) { + + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "%s values passed for property '%s' while maxItems == %s" + .formatted(numberOfValues, property.name(), maxItems)); + } + + long minItems = representation.minItems(); + if (numberOfValues < minItems) { + + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "%s values passed for property '%s' while minItems == %s" + .formatted(numberOfValues, property.name(), minItems)); + } + + @SuppressWarnings("unchecked") + FormProperty stringProperty = (FormProperty) property; + + String valueField = representation.valueField().orElse("value"); + + List inlineElements = representation.inline().orElse(null); + if (inlineElements != null) { + return new TemplateOptionsInlineElements(valueField, inlineElements) + .validate(stringProperty) + .mapTo(property); + } + + OptionsLinkRepresentation optionsLink = representation.link().orElse(null); + if (optionsLink == null) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(false) + .reason( + "Missing options inline and remote elements for property '%s'." + .formatted(property.name())); + } + + return new TemplateOptionsInlineElements(valueField, fetchRemoteElements(optionsLink)) + .validate(stringProperty) + .mapTo(property); + } + + private List fetchRemoteElements( + OptionsLinkRepresentation optionsLink) throws Exception { + + String optionsHref = Link.of(optionsLink.href()).expand().toUri().toString(); + + String rawOptions = + requestExecutor + .execute(get(optionsHref)) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse() + .getContentAsString(); + + return objectMapper.readValue(rawOptions, new TypeReference<>() {}); + } +} diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptionsInlineElement.java b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptionsInlineElement.java new file mode 100644 index 0000000..599a7fc --- /dev/null +++ b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptionsInlineElement.java @@ -0,0 +1,36 @@ +package com.cosium.hal_mock_mvc; + +import static java.util.Objects.requireNonNull; + +import com.cosium.hal_mock_mvc.template.options.InlineElementRepresentation; +import com.cosium.hal_mock_mvc.template.options.MapInlineElementRepresentation; +import com.cosium.hal_mock_mvc.template.options.StringInlineElementRepresentation; + +/** + * @author Réda Housni Alaoui + */ +class TemplateOptionsInlineElement { + + private final String valueField; + private final InlineElementRepresentation representation; + + TemplateOptionsInlineElement(String valueField, InlineElementRepresentation representation) { + this.valueField = requireNonNull(valueField); + this.representation = requireNonNull(representation); + } + + public boolean matches(String value) { + if (representation instanceof MapInlineElementRepresentation mapInlineElementRepresentation) { + + return value.equals(mapInlineElementRepresentation.map().get(valueField)); + + } else if (representation + instanceof StringInlineElementRepresentation stringInlineElementRepresentation) { + + return value.equals(stringInlineElementRepresentation.value()); + + } else { + throw new IllegalArgumentException("Unexpected type %s".formatted(representation.getClass())); + } + } +} diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptionsInlineElements.java b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptionsInlineElements.java new file mode 100644 index 0000000..70669b5 --- /dev/null +++ b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptionsInlineElements.java @@ -0,0 +1,55 @@ +package com.cosium.hal_mock_mvc; + +import static java.util.Objects.requireNonNull; + +import com.cosium.hal_mock_mvc.template.options.InlineElementRepresentation; +import java.util.List; + +/** + * @author Réda Housni Alaoui + */ +class TemplateOptionsInlineElements { + private final String valueField; + private final List representations; + + TemplateOptionsInlineElements( + String valueField, List representations) { + this.valueField = requireNonNull(valueField); + this.representations = List.copyOf(representations); + } + + public ValidatedFormProperty validate(FormProperty property) { + + if (representations.isEmpty()) { + String firstValidationError = + "Value of property '%s' must be null because the list of available option is empty." + .formatted(property.name()); + + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason(firstValidationError); + } + + List inlineElements = + representations.stream() + .map(representation -> new TemplateOptionsInlineElement(valueField, representation)) + .toList(); + + for (String value : property.values()) { + + if (inlineElements.stream().anyMatch(inlineElement -> inlineElement.matches(value))) { + continue; + } + + String firstValidationError = + "Value '%s' didn't match any inline option of property '%s' among %s" + .formatted(value, property.name(), representations); + + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason(firstValidationError); + } + + return ValidatedFormProperty.markAsValid(property); + } +} diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/TemplateProperty.java b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateProperty.java new file mode 100644 index 0000000..6cb549e --- /dev/null +++ b/core/src/main/java/com/cosium/hal_mock_mvc/TemplateProperty.java @@ -0,0 +1,223 @@ +package com.cosium.hal_mock_mvc; + +import static java.util.Objects.requireNonNull; + +import com.cosium.hal_mock_mvc.template.options.OptionsRepresentation; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * @author Réda Housni Alaoui + */ +class TemplateProperty { + + private static final Set STRING_HAL_FORMS_TYPES = + Set.of( + "hidden", + "text", + "textarea", + "search", + "tel", + "url", + "email", + "password", + "date", + "time", + "datetime-local", + "range", + "color"); + + private static final Set NUMBER_HAL_FORMS_TYPES = Set.of("month", "week", "number"); + + private final RequestExecutor requestExecutor; + private final ObjectMapper objectMapper; + private final TemplatePropertyRepresentation representation; + + TemplateProperty( + RequestExecutor requestExecutor, + ObjectMapper objectMapper, + TemplatePropertyRepresentation representation) { + this.requestExecutor = requireNonNull(requestExecutor); + this.objectMapper = requireNonNull(objectMapper); + this.representation = requireNonNull(representation); + } + + public String name() { + return representation.name(); + } + + public Optional defaultValue() { + OptionsRepresentation options = representation.options().orElse(null); + if (options != null) { + return Optional.of(options.selectedValues()) + .filter(Predicate.not(List::isEmpty)) + .map(Object.class::cast); + } + return representation.value().map(Object.class::cast); + } + + public boolean isRequired() { + return representation.required(); + } + + public ValidatedFormProperty validate(FormProperty property) throws Exception { + + Object firstValue = property.values().stream().findFirst().orElse(null); + if (representation.required() && firstValue == null) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Since property '%s' is required, it must hold at least one non-null value." + .formatted(property.name())); + } + + OptionsRepresentation options = representation.options().orElse(null); + if (options != null) { + return new TemplateOptions(requestExecutor, objectMapper, options).validate(property); + } + + if (property.array()) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Given property '%s' has no options attribute, it cannot hold an array of values." + .formatted(property.name())); + } + + String type = representation.type(); + if (STRING_HAL_FORMS_TYPES.contains(type) && !String.class.equals(property.valueType())) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value must be of type String because property '%s' has type '%s'" + .formatted(property.name(), type)); + } + if (NUMBER_HAL_FORMS_TYPES.contains(type) && !property.isNumberValueType()) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value must be of type Number because property '%s' has type '%s'" + .formatted(property.name(), type)); + } + + String regex = representation.regex().orElse(null); + if (regex != null) { + if (!String.class.equals(property.valueType())) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Property '%s' must have a value of type String because it is associated to a regex" + .formatted(property.name())); + } + String stringValue = (String) firstValue; + if (firstValue != null && !Pattern.compile(regex).matcher(stringValue).matches()) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value '%s' of property '%s' does not match regex '%s'" + .formatted(stringValue, property.name(), regex)); + } + } + + Double max = representation.max().orElse(null); + if (max != null) { + if (!property.isNumberValueType()) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value of property '%s' must be a number because the property defines a max value." + .formatted(property.name())); + } + Double doubleValue = property.toDoubleValues().stream().findFirst().orElse(null); + if (doubleValue != null && doubleValue > max) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value %s is greater than the max value of %s of property '%s'." + .formatted(doubleValue, max, property.name())); + } + } + + Double min = representation.min().orElse(null); + if (min != null) { + if (!property.isNumberValueType()) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value of property '%s' must be a number because the property defines a min value." + .formatted(property.name())); + } + Double doubleValue = property.toDoubleValues().stream().findFirst().orElse(null); + if (doubleValue != null && doubleValue < min) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value %s is lower than the min value of %s of property '%s'." + .formatted(doubleValue, min, property.name())); + } + } + + Long maxLength = representation.maxLength().orElse(null); + if (maxLength != null) { + if (!String.class.equals(property.valueType())) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value of property '%s' must be a string because the property defines a max length value." + .formatted(property.name())); + } + String stringValue = (String) firstValue; + if (stringValue != null && stringValue.length() > maxLength) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value '%s' of property '%s' has a greater length than the defined max length of %s" + .formatted(stringValue, property.name(), maxLength)); + } + } + + Long minLength = representation.minLength().orElse(null); + if (minLength != null) { + if (!String.class.equals(property.valueType())) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value of property '%s' must be a string because the property defines a min length value." + .formatted(property.name())); + } + String stringValue = (String) firstValue; + if (stringValue != null && stringValue.length() < minLength) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value '%s' of property '%s' has a lower length than the defined min length of %s" + .formatted(stringValue, property.name(), minLength)); + } + } + + Double step = representation.step().orElse(null); + if (step != null) { + if (!property.isNumberValueType()) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value of property '%s' must be a number because the property defines a step value." + .formatted(property.name())); + } + Double doubleValue = property.toDoubleValues().stream().findFirst().orElse(null); + if (doubleValue != null && Math.round(doubleValue / step) != doubleValue / step) { + return ValidatedFormProperty.invalidBuilder(property) + .serverSideVerifiable(true) + .reason( + "Value '%s' of property '%s' is not compatible with the defined step of '%s'" + .formatted(doubleValue, property.name(), step)); + } + } + + return ValidatedFormProperty.markAsValid(property); + } +} diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/TemplatePropertyRepresentation.java b/core/src/main/java/com/cosium/hal_mock_mvc/TemplatePropertyRepresentation.java index 1ee963b..d3f1ecb 100644 --- a/core/src/main/java/com/cosium/hal_mock_mvc/TemplatePropertyRepresentation.java +++ b/core/src/main/java/com/cosium/hal_mock_mvc/TemplatePropertyRepresentation.java @@ -18,6 +18,13 @@ public class TemplatePropertyRepresentation { private final String regex; private final boolean templated; private final OptionsRepresentation options; + private final boolean readOnly; + private final String type; + private final Double max; + private final Long maxLength; + private final Double min; + private final Long minLength; + private final Double step; @JsonCreator TemplatePropertyRepresentation( @@ -27,7 +34,14 @@ public class TemplatePropertyRepresentation { @JsonProperty("prompt") String prompt, @JsonProperty("regex") String regex, @JsonProperty("templated") Boolean templated, - @JsonProperty("options") OptionsRepresentation options) { + @JsonProperty("options") OptionsRepresentation options, + @JsonProperty("readOnly") Boolean readOnly, + @JsonProperty("type") String type, + @JsonProperty("max") Double max, + @JsonProperty("maxLength") Long maxLength, + @JsonProperty("min") Double min, + @JsonProperty("minLength") Long minLength, + @JsonProperty("step") Double step) { this.name = requireNonNull(name, "Attribute 'name' is missing"); this.required = Optional.ofNullable(required).orElse(false); this.value = value; @@ -35,12 +49,27 @@ public class TemplatePropertyRepresentation { this.regex = regex; this.templated = Optional.ofNullable(templated).orElse(false); this.options = options; + this.readOnly = Optional.ofNullable(readOnly).orElse(false); + this.type = Optional.ofNullable(type).orElse("text"); + this.max = max; + this.maxLength = maxLength; + this.min = min; + this.minLength = minLength; + this.step = step; } public String name() { return name; } + public String type() { + return type; + } + + public boolean readOnly() { + return readOnly; + } + public boolean required() { return required; } @@ -49,6 +78,26 @@ public Optional value() { return Optional.ofNullable(value); } + public Optional max() { + return Optional.ofNullable(max); + } + + public Optional maxLength() { + return Optional.ofNullable(maxLength); + } + + public Optional min() { + return Optional.ofNullable(min); + } + + public Optional minLength() { + return Optional.ofNullable(minLength); + } + + public Optional step() { + return Optional.ofNullable(step); + } + public String prompt() { return prompt; } diff --git a/core/src/main/java/com/cosium/hal_mock_mvc/Templates.java b/core/src/main/java/com/cosium/hal_mock_mvc/Templates.java index 23e5f27..836a05a 100644 --- a/core/src/main/java/com/cosium/hal_mock_mvc/Templates.java +++ b/core/src/main/java/com/cosium/hal_mock_mvc/Templates.java @@ -24,6 +24,7 @@ public class Templates { private final RequestExecutor requestExecutor; + private final ObjectMapper objectMapper; private final HalFormsBody body; Templates(RequestExecutor requestExecutor, ResultActions resultActions) throws Exception { @@ -45,16 +46,18 @@ public class Templates { + jsonBody + "'"); } - body = + objectMapper = new ObjectMapper() .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .registerModule(new JacksonModule()) - .readValue(jsonBody, HalFormsBody.class); + .registerModule(new JacksonModule()); + body = objectMapper.readValue(jsonBody, HalFormsBody.class); } public Optional