Skip to content

Commit

Permalink
Support advanced form submission
Browse files Browse the repository at this point in the history
  • Loading branch information
reda-alaoui committed Feb 26, 2024
1 parent 34c8ba3 commit 3818525
Show file tree
Hide file tree
Showing 11 changed files with 537 additions and 12 deletions.
109 changes: 109 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/Form.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.cosium.hal_mock_mvc;

import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
* @author Réda Housni Alaoui
*/
public class Form {

private final RequestExecutor requestExecutor;
private final ObjectMapper objectMapper;
private final Template template;

private final Map<String, FormProperty<?>> propertyByName = new HashMap<>();

Form(RequestExecutor requestExecutor, ObjectMapper objectMapper, Template template) {
this.requestExecutor = requireNonNull(requestExecutor);
this.objectMapper = requireNonNull(objectMapper);
this.template = requireNonNull(template);
}

public Form withStringValue(String propertyName, String value) throws Exception {
FormProperty<?> property =
new FormProperty<>(String.class, propertyName, List.of(value), false);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withBooleanValue(String propertyName, Boolean value) throws Exception {
FormProperty<?> property =
new FormProperty<>(Boolean.class, propertyName, List.of(value), false);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withIntegerValue(String propertyName, Integer value) throws Exception {
FormProperty<?> property =
new FormProperty<>(Integer.class, propertyName, List.of(value), false);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withLongValue(String propertyName, Long value) throws Exception {
FormProperty<?> property = new FormProperty<>(Long.class, propertyName, List.of(value), false);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withDoubleValue(String propertyName, Double value) throws Exception {
FormProperty<?> property =
new FormProperty<>(Double.class, propertyName, List.of(value), false);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withStringValues(String propertyName, List<String> value) throws Exception {
FormProperty<?> property = new FormProperty<>(String.class, propertyName, value, true);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withBooleanValues(String propertyName, List<Boolean> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Boolean.class, propertyName, value, true);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withIntValues(String propertyName, List<Integer> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Integer.class, propertyName, value, true);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withLongValues(String propertyName, List<Long> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Long.class, propertyName, value, true);
propertyByName.put(property.name(), assertValid(property));
return this;
}

public Form withDoubleValues(String propertyName, List<Double> value) throws Exception {
FormProperty<?> property = new FormProperty<>(Double.class, propertyName, value, true);
propertyByName.put(property.name(), assertValid(property));
return this;
}

private FormProperty<?> assertValid(FormProperty<?> property) throws Exception {
TemplatePropertyRepresentation representation = requireTemplate(property);
if (representation.readOnly()) {
throw new IllegalArgumentException(
"Cannot set value for read-only property '%s'".formatted(property.name()));
}
return new TemplateProperty(requestExecutor, objectMapper, representation)
.assertValid(property);
}

private TemplatePropertyRepresentation requireTemplate(FormProperty<?> property) {
return Optional.ofNullable(template.representation().propertyByName().get(property.name()))
.orElseThrow(
() ->
new IllegalArgumentException(
"No property found for name '%s'".formatted(property.name())));
}
}
58 changes: 58 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/FormProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.cosium.hal_mock_mvc;

import static java.util.Objects.requireNonNull;

import java.util.List;
import java.util.Set;

/**
* @author Réda Housni Alaoui
*/
record FormProperty<T>(Class<T> valueType, String name, List<T> 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 = List.copyOf(values);
}

public boolean isNumberValueType() {
return Set.of(Integer.class, Long.class, Double.class).contains(valueType);
}

public List<Double> 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));
}
}
}
8 changes: 8 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/Template.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -21,17 +23,23 @@ 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);

httpMethod = representation.method().toUpperCase();
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);
Expand Down
92 changes: 92 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/TemplateOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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 FormProperty<?> assertValid(FormProperty<?> property) throws Exception {
if (!String.class.equals(property.valueType())) {
throw new IllegalArgumentException(
"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) {
throw new IllegalArgumentException(
"%s values passed for property '%s' while maxItems == %s"
.formatted(numberOfValues, property.name(), maxItems));
}

long minItems = representation.minItems();
if (numberOfValues < minItems) {
throw new IllegalArgumentException(
"%s values passed for property '%s' while minItems == %s"
.formatted(numberOfValues, property.name(), minItems));
}

@SuppressWarnings("unchecked")
FormProperty<String> stringProperty = (FormProperty<String>) property;

String valueField = representation.valueField().orElse("value");

List<InlineElementRepresentation> inlineElements = representation.inline().orElse(null);
if (inlineElements != null) {
return new TemplateOptionsInlineElements(valueField, inlineElements)
.assertValuesAreValidOptions(stringProperty);
}

OptionsLinkRepresentation optionsLink = representation.link().orElse(null);
if (optionsLink == null) {
throw new IllegalArgumentException(
"Missing inline and remote elements from options %s.".formatted(representation));
}

return new TemplateOptionsInlineElements(valueField, fetchRemoteElements(optionsLink))
.assertValuesAreValidOptions(stringProperty);
}

private List<InlineElementRepresentation> 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<>() {});
}
}
Original file line number Diff line number Diff line change
@@ -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()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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<InlineElementRepresentation> representations;

TemplateOptionsInlineElements(
String valueField, List<InlineElementRepresentation> representations) {
this.valueField = requireNonNull(valueField);
this.representations = List.copyOf(representations);
}

public FormProperty<String> assertValuesAreValidOptions(FormProperty<String> property) {

if (representations.isEmpty()) {
throw new IllegalArgumentException(
"Value of property '%s' cannot be non null because the list of available option is empty."
.formatted(property.name()));
}

List<TemplateOptionsInlineElement> inlineElements =
representations.stream()
.map(representation -> new TemplateOptionsInlineElement(valueField, representation))
.toList();

for (String value : property.values()) {

if (inlineElements.stream().anyMatch(inlineElement -> inlineElement.matches(value))) {
continue;
}

throw new IllegalArgumentException(
"Value '%s' didn't match any inline option of property '%s' among %s"
.formatted(value, property.name(), representations));
}

return property;
}
}
Loading

0 comments on commit 3818525

Please sign in to comment.