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 4a032d9
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 11 deletions.
104 changes: 104 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,104 @@
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 {
return new TemplateProperty(requestExecutor, objectMapper, requireTemplate(property))
.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())));
}
}
8 changes: 8 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,8 @@
package com.cosium.hal_mock_mvc;

import java.util.List;

/**
* @author Réda Housni Alaoui
*/
record FormProperty<T>(Class<T> valueType, String name, List<T> values, boolean array) {}
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
76 changes: 76 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,76 @@
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(
"Cannot set a value of type '%s' because property '%s' expects a value from an enumeration of options."
.formatted(property.valueType(), property.name()));
}

@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;
}
}
33 changes: 33 additions & 0 deletions core/src/main/java/com/cosium/hal_mock_mvc/TemplateProperty.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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;

/**
* @author Réda Housni Alaoui
*/
class TemplateProperty {

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 FormProperty<?> assertValid(FormProperty<?> property) throws Exception {
OptionsRepresentation options = representation.options().orElse(null);
if (options != null) {
return new TemplateOptions(requestExecutor, objectMapper, options).assertValid(property);
}
return property;
}
}
16 changes: 11 additions & 5 deletions core/src/main/java/com/cosium/hal_mock_mvc/Templates.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<Template> byOptionalKey(String key) {
return Optional.ofNullable(body.templateByKey.get(key))
.map(representation -> new Template(requestExecutor, body.baseUri, key, representation));
.map(
representation ->
new Template(requestExecutor, objectMapper, body.baseUri, key, representation));
}

public Template byKey(String key) {
Expand All @@ -64,7 +67,10 @@ public Template byKey(String key) {

public Collection<Template> list() {
return body.templateByKey.entrySet().stream()
.map(entry -> new Template(requestExecutor, body.baseUri, entry.getKey(), entry.getValue()))
.map(
entry ->
new Template(
requestExecutor, objectMapper, body.baseUri, entry.getKey(), entry.getValue()))
.collect(Collectors.toSet());
}

Expand Down
Loading

0 comments on commit 4a032d9

Please sign in to comment.