From 48c8b9b5996266ff6b64f531b79e10128bc113d2 Mon Sep 17 00:00:00 2001 From: Nicolas Senave Date: Mon, 29 Apr 2024 17:54:28 +0200 Subject: [PATCH] feat: duration component --- pom.xml | 2 +- .../lunatic/model/flat/ComponentType.java | 1 + .../lunatic/model/flat/ComponentTypeEnum.java | 1 + .../fr/insee/lunatic/model/flat/Duration.java | 42 ++++++++ .../fr/insee/lunatic/model/flat/Question.java | 2 +- .../conversion/DurationSerializationTest.java | 95 +++++++++++++++++++ .../resources/duration-hours-minutes.json | 22 +++++ src/test/resources/duration-years-months.json | 22 +++++ 8 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/main/java/fr/insee/lunatic/model/flat/Duration.java create mode 100644 src/test/java/fr/insee/lunatic/conversion/DurationSerializationTest.java create mode 100644 src/test/resources/duration-hours-minutes.json create mode 100644 src/test/resources/duration-years-months.json diff --git a/pom.xml b/pom.xml index d88f242..d64eec6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ lunatic-model jar - 3.7.0 + 3.8.0 Lunatic Model Classes and converters for the Lunatic model https://inseefr.github.io/Lunatic-Model/ diff --git a/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java b/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java index ebc18b8..dbe89a4 100644 --- a/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java +++ b/src/main/java/fr/insee/lunatic/model/flat/ComponentType.java @@ -66,6 +66,7 @@ @JsonSubTypes.Type(value = Input.class, name = "Input"), @JsonSubTypes.Type(value = PairwiseLinks.class, name = "PairwiseLinks"), @JsonSubTypes.Type(value = Datepicker.class, name = "Datepicker"), + @JsonSubTypes.Type(value = Duration.class, name = "Duration"), @JsonSubTypes.Type(value = CheckboxGroup.class, name = "CheckboxGroup"), @JsonSubTypes.Type(value = CheckboxOne.class, name = "CheckboxOne"), @JsonSubTypes.Type(value = CheckboxBoolean.class, name = "CheckboxBoolean"), diff --git a/src/main/java/fr/insee/lunatic/model/flat/ComponentTypeEnum.java b/src/main/java/fr/insee/lunatic/model/flat/ComponentTypeEnum.java index 6f4ec41..255b134 100644 --- a/src/main/java/fr/insee/lunatic/model/flat/ComponentTypeEnum.java +++ b/src/main/java/fr/insee/lunatic/model/flat/ComponentTypeEnum.java @@ -16,6 +16,7 @@ public enum ComponentTypeEnum { PAIRWISE_LINKS("PairwiseLinks"), INPUT_NUMBER("InputNumber"), DATEPICKER("Datepicker"), + DURATION("Duration"), CHECKBOX_GROUP("CheckboxGroup"), CHECKBOX_ONE("CheckboxOne"), CHECKBOX_BOOLEAN("CheckboxBoolean"), diff --git a/src/main/java/fr/insee/lunatic/model/flat/Duration.java b/src/main/java/fr/insee/lunatic/model/flat/Duration.java new file mode 100644 index 0000000..9e9025b --- /dev/null +++ b/src/main/java/fr/insee/lunatic/model/flat/Duration.java @@ -0,0 +1,42 @@ +package fr.insee.lunatic.model.flat; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Getter +@Slf4j +public class Duration extends ComponentType implements ComponentSimpleResponseType { + + /** Value for a years/months duration format. */ + public static final String YEARS_MONTHS_FORMAT = "PnYnM"; + + /** Value for a hours/minutes duration format. */ + public static final String HOURS_MINUTES_FORMAT = "PTnHnM"; + + public Duration() { + this.componentType = ComponentTypeEnum.DURATION; + } + + /** Duration format in the XSD Duration Data Type style. + * Must start with a 'P' (that stands for period). Then can be followed by 'nY' (years), 'nM' (months), 'nD' (days). + * 'T' indicates the start of a time section that can be followed by 'nH' (hours), 'nM' (minutes), 'nS' (seconds). */ + @JsonProperty(required = true) + private String format; + + @JsonProperty(required = true) + @Setter + protected ResponseType response; + + /** + * Sets the duration format. Warning: this method doesn't do any strict validation. + * @param format Format in the XSD Duration Data Type style. + */ + public void setFormat(String format) { + if (! (YEARS_MONTHS_FORMAT.equals(format) || HOURS_MINUTES_FORMAT.equals(format))) + log.warn("Format '{}' does not match Lunatic commonly-used formats.", format); + this.format = format; + } + +} diff --git a/src/main/java/fr/insee/lunatic/model/flat/Question.java b/src/main/java/fr/insee/lunatic/model/flat/Question.java index 18bbd39..53e0062 100644 --- a/src/main/java/fr/insee/lunatic/model/flat/Question.java +++ b/src/main/java/fr/insee/lunatic/model/flat/Question.java @@ -56,7 +56,7 @@ public static boolean isQuestionComponent(ComponentType component) { if (component.getComponentType() == null) return false; return switch (component.getComponentType()) { - case CHECKBOX_BOOLEAN, INPUT, TEXTAREA, INPUT_NUMBER, DATEPICKER, + case CHECKBOX_BOOLEAN, INPUT, TEXTAREA, INPUT_NUMBER, DATEPICKER, DURATION, CHECKBOX_ONE, RADIO, DROPDOWN, SUGGESTER, CHECKBOX_GROUP, TABLE, ROSTER_FOR_LOOP, PAIRWISE_LINKS -> true; case QUESTIONNAIRE, SEQUENCE, SUBSEQUENCE, QUESTION, LOOP -> false; diff --git a/src/test/java/fr/insee/lunatic/conversion/DurationSerializationTest.java b/src/test/java/fr/insee/lunatic/conversion/DurationSerializationTest.java new file mode 100644 index 0000000..6bbdc56 --- /dev/null +++ b/src/test/java/fr/insee/lunatic/conversion/DurationSerializationTest.java @@ -0,0 +1,95 @@ +package fr.insee.lunatic.conversion; + +import fr.insee.lunatic.exception.SerializationException; +import fr.insee.lunatic.model.flat.*; +import fr.insee.lunatic.utils.TestUtils; +import org.json.JSONException; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +class DurationSerializationTest { + + @Test + void serializeDuration_yearsMonthsFormat() throws SerializationException, IOException, JSONException { + // + Questionnaire questionnaire = new Questionnaire(); + questionnaire.setId("questionnaire-id"); + // + Duration duration = new Duration(); + duration.setId("duration-id"); + duration.setFormat(Duration.YEARS_MONTHS_FORMAT); + duration.setLabel(new LabelType()); + duration.getLabel().setValue("\"Duration (years/months)\""); + duration.getLabel().setType(LabelTypeEnum.VTL_MD); + duration.setDescription(new LabelType()); + duration.getDescription().setValue("\"2 years and 5 months\""); + duration.getDescription().setType(LabelTypeEnum.VTL_MD); + duration.setResponse(new ResponseType()); + duration.getResponse().setName("DURATION_VAR"); + // + questionnaire.getComponents().add(duration); + + // + String result = new JsonSerializer().serialize(questionnaire); + + String expectedJson = TestUtils.readResourceFile("duration-years-months.json"); + JSONAssert.assertEquals(expectedJson, result, JSONCompareMode.STRICT); + } + + @Test + void serializeDuration_HoursMinutesFormat() throws SerializationException, IOException, JSONException { + // + Questionnaire questionnaire = new Questionnaire(); + questionnaire.setId("questionnaire-id"); + // + Duration duration = new Duration(); + duration.setId("duration-id"); + duration.setFormat(Duration.HOURS_MINUTES_FORMAT); + duration.setLabel(new LabelType()); + duration.getLabel().setValue("\"Duration (hours/minutes)\""); + duration.getLabel().setType(LabelTypeEnum.VTL_MD); + duration.setDescription(new LabelType()); + duration.getDescription().setValue("\"1 hour and 10 minutes\""); + duration.getDescription().setType(LabelTypeEnum.VTL_MD); + duration.setResponse(new ResponseType()); + duration.getResponse().setName("DURATION_VAR"); + // + questionnaire.getComponents().add(duration); + + // + String result = new JsonSerializer().serialize(questionnaire); + + String expectedJson = TestUtils.readResourceFile("duration-hours-minutes.json"); + JSONAssert.assertEquals(expectedJson, result, JSONCompareMode.STRICT); + } + + @Test + void deserializeDuration_yearsMonthsFormat() throws IOException, SerializationException { + // + String jsonInput = TestUtils.readResourceFile("duration-years-months.json"); + // + Questionnaire questionnaire = new JsonDeserializer().deserialize(new ByteArrayInputStream(jsonInput.getBytes())); + // + Duration duration = assertInstanceOf(Duration.class, questionnaire.getComponents().getFirst()); + assertEquals("PnYnM", duration.getFormat()); + } + + @Test + void deserializeDuration_hoursMinutesFormat() throws IOException, SerializationException { + // + String jsonInput = TestUtils.readResourceFile("duration-hours-minutes.json"); + // + Questionnaire questionnaire = new JsonDeserializer().deserialize(new ByteArrayInputStream(jsonInput.getBytes())); + // + Duration duration = assertInstanceOf(Duration.class, questionnaire.getComponents().getFirst()); + assertEquals("PTnHnM", duration.getFormat()); + } + +} diff --git a/src/test/resources/duration-hours-minutes.json b/src/test/resources/duration-hours-minutes.json new file mode 100644 index 0000000..90e6e5a --- /dev/null +++ b/src/test/resources/duration-hours-minutes.json @@ -0,0 +1,22 @@ +{ + "id": "questionnaire-id", + "componentType": "Questionnaire", + "components": [ + { + "id": "duration-id", + "componentType": "Duration", + "format": "PTnHnM", + "label": { + "value": "\"Duration (hours/minutes)\"", + "type": "VTL|MD" + }, + "description": { + "value": "\"1 hour and 10 minutes\"", + "type": "VTL|MD" + }, + "response": { + "name": "DURATION_VAR" + } + } + ] +} diff --git a/src/test/resources/duration-years-months.json b/src/test/resources/duration-years-months.json new file mode 100644 index 0000000..f8ad731 --- /dev/null +++ b/src/test/resources/duration-years-months.json @@ -0,0 +1,22 @@ +{ + "id": "questionnaire-id", + "componentType": "Questionnaire", + "components": [ + { + "id": "duration-id", + "componentType": "Duration", + "format": "PnYnM", + "label": { + "value": "\"Duration (years/months)\"", + "type": "VTL|MD" + }, + "description": { + "value": "\"2 years and 5 months\"", + "type": "VTL|MD" + }, + "response": { + "name": "DURATION_VAR" + } + } + ] +}