diff --git a/wres-config/src/wres/config/yaml/DeclarationFactory.java b/wres-config/src/wres/config/yaml/DeclarationFactory.java index 785835495..83e9a6759 100644 --- a/wres-config/src/wres/config/yaml/DeclarationFactory.java +++ b/wres-config/src/wres/config/yaml/DeclarationFactory.java @@ -47,6 +47,7 @@ import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; +import org.yaml.snakeyaml.constructor.DuplicateKeyException; import org.yaml.snakeyaml.nodes.Tag; import org.yaml.snakeyaml.parser.ParserException; import org.yaml.snakeyaml.representer.Representer; @@ -418,7 +419,7 @@ static JsonNode deserialize( String yaml ) throws IOException // Deserialize with Jackson now that any anchors/references are resolved return DESERIALIZER.readTree( resolvedYamlString ); } - catch ( ScannerException | ParserException | JsonProcessingException e ) + catch ( ScannerException | ParserException | JsonProcessingException | DuplicateKeyException e ) { throw new IOException( "Failed to deserialize a YAML string.", e ); } diff --git a/wres-config/src/wres/config/yaml/components/Features.java b/wres-config/src/wres/config/yaml/components/Features.java index aab319103..170f7fbd9 100644 --- a/wres-config/src/wres/config/yaml/components/Features.java +++ b/wres-config/src/wres/config/yaml/components/Features.java @@ -31,6 +31,7 @@ public record Features( Set geometries, Map /** * Sets the default values. * @param geometries the geometries + * @param offsets the offset values, such as a datum offset, if any */ public Features { diff --git a/wres-config/src/wres/config/yaml/components/Threshold.java b/wres-config/src/wres/config/yaml/components/Threshold.java index 8788fb9ad..14182057d 100644 --- a/wres-config/src/wres/config/yaml/components/Threshold.java +++ b/wres-config/src/wres/config/yaml/components/Threshold.java @@ -38,6 +38,7 @@ public record Threshold( wres.statistics.generated.Threshold threshold, * @param type the threshold type to help identify the declaration context * @param feature a feature * @param featureNameFrom the orientation of the data from which the named feature is taken + * @param generated true if this threshold was generated by the software, false if declared or read from a source */ public Threshold { @@ -65,6 +66,7 @@ public String toString() .append( "thresholdType", this.type() ) .append( "featureName", featureString ) .append( "featureNameFrom", this.featureNameFrom() ) + .append( "generated", this.generated() ) .build(); } } diff --git a/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java b/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java index cdc2b69fd..760996ef9 100644 --- a/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java +++ b/wres-config/test/wres/config/yaml/DeclarationFactoryTest.java @@ -27,6 +27,7 @@ import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import wres.config.MetricConstants; import wres.config.yaml.components.AnalysisTimes; @@ -2274,6 +2275,36 @@ void testDeserializeWithCovariates() throws IOException assertEquals( expected, actual ); } + @Test + void testDeserializeThrowsExpectedExceptionWhenDuplicateKeysEncountered() + { + // GitHub issue #336 + String yaml = """ + observed: + - some_file.csv + predicted: + sources: another_file.csv + probability_thresholds: [0.01,0.1,0.5,0.9,0.95,0.99,0.995,0.999] + probability_thresholds: [0.01,0.1,0.5,0.9,0.95,0.99,0.995,0.999] + """; + + assertThrows( IOException.class, () -> DeclarationFactory.from( yaml ) ); + } + + @Test + void testDeserializeThrowsExpectedExceptionWhenSpacingIncorrect() + { + String yaml = """ + observed: + - some_file.csv + predicted: + sources: another_file.csv + probability_thresholds: [0.01,0.1,0.5,0.9,0.95,0.99,0.995,0.999] + """; + + assertThrows( IOException.class, () -> DeclarationFactory.from( yaml ) ); + } + @Test void testSerializeWithShortSources() throws IOException {