blob: a5acb3f34cd46cb341e56c4ef57fef4bd13c8c8b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.apache.tuweni.config;
import static org.apache.tuweni.config.ConfigurationErrors.noErrors;
import static org.apache.tuweni.config.ConfigurationErrors.singleError;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.NoSuchFileException;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.opentest4j.AssertionFailedError;
class TomlBackedConfigurationTest {
@Test
void loadSimpleConfigFile() throws Exception {
// @formatter:off
Configuration config = Configuration.fromToml(
"foo = \"12\"\n"
+ "bar = 13\n"
+ "foobar = 156.34\n"
+ "amaps = [{a = 1, b = 2}, {a = 'hello'}]\n"
+ "\n"
+ "[boo]\n"
+ "baz=[1,2,3]\n"
+ "\n"
+ "[amap]\n"
+ "a=1\n"
+ "b=2\n"
+ "[deepmap]\n"
+ "a=4\n"
+ "[deepmap.deeper]\n"
+ "b=5\n"
);
// @formatter:on
assertFalse(config.hasErrors());
assertEquals("12", config.getString("foo"));
assertEquals(13, config.getInteger("bar"));
assertEquals(13L, config.getLong("bar"));
assertEquals(156.34d, config.getDouble("foobar"));
assertEquals(Arrays.asList(1, 2, 3), config.getListOfInteger("boo.baz"));
assertEquals(Arrays.asList(1L, 2L, 3L), config.getListOfLong("boo.baz"));
assertEquals(Arrays.asList(1L, 2L, 3L), config.getList("boo.baz"));
Map<String, Object> expectedMap = new HashMap<>();
expectedMap.put("a", 1L);
expectedMap.put("b", 2L);
assertEquals(expectedMap, config.getMap("amap"));
assertEquals(Collections.singletonMap("baz", Arrays.asList(1L, 2L, 3L)), config.getMap("boo"));
Map<String, Object> expectedDeepMap = new HashMap<>();
expectedDeepMap.put("a", 4L);
expectedDeepMap.put("deeper", Collections.singletonMap("b", 5L));
assertEquals(expectedDeepMap, config.getMap("deepmap"));
List<Map<String, Object>> expectedList = Arrays.asList(expectedMap, Collections.singletonMap("a", "hello"));
assertEquals(expectedList, config.getListOfMap("amaps"));
}
@Test
void testKeyPresent() throws Exception {
Configuration config = Configuration.fromToml("foo=\"12\"\nbar=\"13\"\n[baz]\nfoobar = 156.34");
assertFalse(config.hasErrors());
assertEquals(new HashSet<>(Arrays.asList("foo", "bar", "baz.foobar")), config.keySet());
assertTrue(config.contains("foo"));
assertTrue(config.contains("bar"));
assertTrue(config.contains("baz.foobar"));
assertFalse(config.contains("example"));
}
@Test
void testFindsValuesBasedOnCanonicalKey() throws Exception {
Configuration config = Configuration.fromToml("foo=12\nbar=\"13\"\n[baz]\nfoobar = 156.34");
assertFalse(config.hasErrors());
assertEquals(new HashSet<>(Arrays.asList("foo", "bar", "baz.foobar")), config.keySet());
assertEquals(12, config.getInteger("foo"));
assertEquals(12, config.getInteger(" foo "));
assertEquals(12, config.getInteger(" 'foo' "));
assertEquals(12, config.getInteger(" \"foo\" "));
assertTrue(config.contains("baz.foobar"));
assertTrue(config.contains("baz . foobar"));
assertTrue(config.contains("baz . 'foobar'"));
assertTrue(config.contains("\"baz\" . 'foobar'"));
}
@Test
void throwsForMissingValue() throws Exception {
Configuration config = Configuration.fromToml("foo=12\nbar=\"13\"\n[baz]\nfoobar = 156.34");
assertFalse(config.hasErrors());
Exception e = assertThrows(NoConfigurationPropertyException.class, () -> config.getString("foo.blah"));
assertEquals("No value for property 'foo.blah'", e.getMessage());
e = assertThrows(NoConfigurationPropertyException.class, () -> config.getInteger(" foobaz "));
assertEquals("No value for property 'foobaz'", e.getMessage());
}
@Test
void throwsForInvalidType() throws Exception {
Configuration config = Configuration.fromToml(
"[foo]\n" + "bar=99\n" + "baz='buz'\n" + "biz=false\n" + "buz = [1,2,3]\n" + "sbuz = ['1', '2', '3']\n");
assertFalse(config.hasErrors());
assertEquals(99, config.getInteger("foo.bar"));
InvalidConfigurationPropertyTypeException e =
assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getString("foo.bar"));
assertEquals("Value of 'foo.bar' is a integer", e.getMessage());
assertEquals(DocumentPosition.positionAt(2, 1), e.position());
assertEquals("buz", config.getString("foo.baz"));
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getDouble("foo.baz"));
assertEquals("Value of 'foo.baz' is a string", e.getMessage());
assertEquals(DocumentPosition.positionAt(3, 1), e.position());
assertFalse(config.getBoolean("foo.biz"));
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getLong("foo.biz"));
assertEquals("Value of 'foo.biz' is a boolean", e.getMessage());
assertEquals(DocumentPosition.positionAt(4, 1), e.position());
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getLong("foo"));
assertEquals("Value of 'foo' is a table", e.getMessage());
assertEquals(DocumentPosition.positionAt(1, 1), e.position());
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfString("foo.buz"));
assertEquals("List property 'foo.buz' does not contain strings", e.getMessage());
assertEquals(DocumentPosition.positionAt(5, 1), e.position());
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfInteger("foo.sbuz"));
assertEquals("List property 'foo.sbuz' does not contain integers", e.getMessage());
assertEquals(DocumentPosition.positionAt(6, 1), e.position());
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfMap("foo.sbuz"));
assertEquals("List property 'foo.sbuz' does not contain maps", e.getMessage());
assertEquals(DocumentPosition.positionAt(6, 1), e.position());
}
@Test
void throwsForImpossibleIntegerConversion() throws Exception {
// @formatter:off
Configuration config = Configuration.fromToml(
"[foo]\n"
+ "\" bar\" = "
+ (1L + Integer.MAX_VALUE)
+ "\n"
+ "buz = [1, 2, "
+ (1L + Integer.MAX_VALUE)
+ ", 4]\n"
);
// @formatter:on
assertFalse(config.hasErrors());
Exception e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getInteger("foo.' bar'"));
assertEquals("Value of property 'foo.\" bar\"' is too large for an integer", e.getMessage());
e = assertThrows(InvalidConfigurationPropertyTypeException.class, () -> config.getListOfInteger("foo.buz"));
assertEquals("Value of property 'foo.buz', index 2, is too large for an integer", e.getMessage());
}
@Test
void loadMissingFile() {
assertThrows(NoSuchFileException.class, () -> {
Configuration.fromToml(Paths.get("FileThatDoesntExist"));
});
}
@Test
void invalidTOMLFile() throws Exception {
Configuration config = Configuration.fromToml("foo=\"12\"\nfoobar = \"156.34");
assertTrue(config.hasErrors());
assertEquals("Unexpected end of input, expected \" or a character", config.errors().get(0).getMessage());
assertEquals(DocumentPosition.positionAt(2, 17), config.errors().get(0).position());
assertEquals("12", config.getString("foo"));
}
@Test
void getDefaultValue() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("foo", "goodbye", null, null);
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml("foobar = 'hello'", schema);
assertEquals("hello", config.getString("foobar"));
assertTrue(config.contains("foo"));
assertEquals("goodbye", config.getString("foo"));
}
@Test
void keysContainSchemaKeys() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("foo", "bar", null, null);
builder.addString("foo.bar", "buz", null, null);
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml("foobar = 'hello'", schema);
assertEquals(new HashSet<>(Arrays.asList("foo", "foo.bar", "foobar")), config.keySet());
}
@Test
void validateConfiguration() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.validateConfiguration(config -> {
if (config.getInteger("expenses") > config.getInteger("revenue")) {
return singleError("Expenses cannot be larger than revenue");
}
return noErrors();
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml("expenses = 2000\nrevenue = 1500\n", schema);
assertTrue(config.hasErrors());
assertEquals("Expenses cannot be larger than revenue", config.errors().get(0).getMessage());
}
@Test
void validateConfigurationProperty() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("foo", "foobar", null, (key, position, value) -> {
if ("bar".equals(value)) {
return singleError(position, "No bar allowed");
}
return noErrors();
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = \"bar\"", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("No bar allowed", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
}
@Test
void validateIntegerProperty() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addInteger("foo", null, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = " + (1L + Integer.MAX_VALUE) + "\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Value of property 'foo' is too large for an integer", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
}
@Test
void shouldValidateStringList() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addListOfString("foo", null, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = [1, 2]\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Value of property 'foo', index 0, is not a string", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
}
@Test
void shouldValidateIntegerList() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addListOfInteger("foo", null, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = ['a', 'b']\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Value of property 'foo', index 0, is not an integer", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
}
@Test
void shouldValidateLongListAsIntegers() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addListOfInteger("foo", null, null, (key, position, value) -> {
assertEquals(Arrays.asList(1, 2, 3), value);
return singleError("should reach here");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = [1, 2, 3]\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("should reach here", error.getMessage());
assertNull(error.position());
}
@Test
void shouldValidateWithinIntegerList() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addListOfInteger("foo", null, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = [1, 2, " + (1L + Integer.MAX_VALUE) + ", 3]\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Value of property 'foo', index 2, is too large for an integer", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
}
@Test
void shouldValidateLongList() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addListOfLong("foo", null, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = ['a', 'b']\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Value of property 'foo', index 0, is not a long", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
}
@Test
void shouldValidateWithIsPresent() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("foo.bar", null, null, PropertyValidator.isPresent());
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = ['a', 'b']\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Required property 'foo.bar' is missing", error.getMessage());
assertNull(error.position());
}
@Test
void shouldValidateWithInRange() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addInteger("foo", null, null, PropertyValidator.inRange(0, 10));
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml(" foo = 15\n", schema);
assertTrue(config.hasErrors());
ConfigurationError error = config.errors().get(0);
assertEquals("Value of property 'foo' is outside range [0,10)", error.getMessage());
assertEquals(DocumentPosition.positionAt(1, 2), error.position());
Configuration config2 = Configuration.fromToml(" foo = 9\n", schema);
assertFalse(config2.hasErrors());
}
@Test
void validatorNotCalledWhenDefaultUsed() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("fooS", "hello", null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
builder.addInteger("fooI", 2, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
builder.addLong("fooL", 2L, null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
builder.addListOfString("fooLS", Collections.emptyList(), null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
builder.addListOfInteger("fooLI", Collections.emptyList(), null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
builder.addListOfLong("fooLL", Collections.emptyList(), null, (key, position, value) -> {
throw new AssertionFailedError("should not be reached");
});
Schema schema = builder.toSchema();
Configuration config = Configuration.fromToml("\n", schema);
assertFalse(config.hasErrors());
}
@Test
void writeConfigurationToToml() throws Exception {
// @formatter:off
Configuration config = Configuration.fromToml(
("foo = \"12\"\n"
+ "bar = 13\n"
+ "foobar = 156.34\n"
+ "amaps = [{a = 1, b = 2}, {a = 'hello'}]\n"
+ "\n"
+ "[boo]\n"
+ "baz=[1,2,3]\n"
+ "\n"
+ "[amap]\n"
+ "a=1\n"
+ "b=2\n"
+ "[deepmap]\n"
+ "' a' = 4\n"
+ "[deepmap.' deep']\n"
+ "'' = 'emptykey'\n"
+ "[deepmap.deeper]\n"
+ "farewell='goodbye'\n").replace("\n", System.lineSeparator())
);
// @formatter:on
// @formatter:off
String expected =
("amaps = [{a = 1, b = 2}, {a = \"hello\"}]\n"
+ "bar = 13\n"
+ "foo = \"12\"\n"
+ "foobar = 156.34\n"
+ "\n"
+ "[amap]\n"
+ "a = 1\n"
+ "b = 2\n"
+ "\n"
+ "[boo]\n"
+ "baz = [1, 2, 3]\n"
+ "\n"
+ "[deepmap]\n"
+ "\" a\" = 4\n"
+ "\n"
+ "[deepmap.\" deep\"]\n"
+ "\"\" = \"emptykey\"\n"
+ "\n"
+ "[deepmap.deeper]\n"
+ "farewell = \"goodbye\"\n").replace("\n", System.lineSeparator());
// @formatter:on
assertEquals(expected, config.toToml());
}
@Test
void writeConfigurationWithDocumentationToToml() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.documentProperty("deepmap", "This table will have sub-tables");
builder.documentProperty("boo.baz", "A list of longs");
builder.documentProperty("bar", "Lucky number 13");
Schema schema = builder.toSchema();
// @formatter:off
Configuration config = Configuration.fromToml(
("foo = \"12\"\n"
+ "bar = 13\n"
+ "foobar = 156.34\n"
+ "amaps = [{a = 1, b = 2}, {a = 'hello'}]\n"
+ "\n"
+ "[boo]\n"
+ "baz=[1,2,3]\n"
+ "\n"
+ "[amap]\n"
+ "a=1\n"
+ "b=2\n"
+ "[deepmap]\n"
+ "' a' = 4\n"
+ "[deepmap.' deep']\n"
+ "'' = 'emptykey'\n"
+ "[deepmap.deeper]\n"
+ "farewell='goodbye'\n").replace("\n", System.lineSeparator()), schema);
// @formatter:on
// @formatter:off
String expected =
("amaps = [{a = 1, b = 2}, {a = \"hello\"}]\n"
+ "## Lucky number 13\n"
+ "bar = 13\n"
+ "foo = \"12\"\n"
+ "foobar = 156.34\n"
+ "\n"
+ "[amap]\n"
+ "a = 1\n"
+ "b = 2\n"
+ "\n"
+ "[boo]\n"
+ "## A list of longs\n"
+ "baz = [1, 2, 3]\n"
+ "\n"
+ "## This table will have sub-tables\n"
+ "[deepmap]\n"
+ "\" a\" = 4\n"
+ "\n"
+ "[deepmap.\" deep\"]\n"
+ "\"\" = \"emptykey\"\n"
+ "\n"
+ "[deepmap.deeper]\n"
+ "farewell = \"goodbye\"\n").replace("\n", System.lineSeparator());
// @formatter:on
assertEquals(expected, config.toToml());
}
@Test
void writeConfigurationToTomlIncludesDefaults() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("farewell", "goodbye", "a farewell", null);
builder.addInteger("xxx", 10, "documentation\n over multiple lines!", null);
builder.addInteger("zzz", 10, null, null);
Schema schema = builder.toSchema();
// @formatter:off
Configuration config = Configuration.fromToml(
"foo = \"12\"\n"
+ "foobar = 156.34\n"
+ "xxx = 10\n"
+ "zzz = 5\n"
+ "\n"
+ "[boo]\n"
+ "baz=[1,2,3]\n"
+ "\n", schema);
// @formatter:on
// @formatter:off
String expected =
"## a farewell\n"
+ "#farewell = \"goodbye\"\n"
+ "foo = \"12\"\n"
+ "foobar = 156.34\n"
+ "## documentation\n"
+ "## over multiple lines!\n"
+ "#xxx = 10\n"
+ "#zzz = 10\n"
+ "zzz = 5\n"
+ "\n"
+ "[boo]\n"
+ "baz = [1, 2, 3]\n";
// @formatter:on
assertEquals(expected.replace("\n", System.lineSeparator()), config.toToml());
}
@Test
void buildSchemaAndDumpToToml() throws Exception {
SchemaBuilder builder = SchemaBuilder.create();
builder.addString("somekey", "somevalue", "Got milk", null);
builder.addBoolean("foo", false, "Toggle switch", null);
builder.addDouble("bar", 1.0, "Value of currency", null);
builder.addLong("'Here now'", 123L, "One two three", null);
builder.addString("'No defaults'", null, null, null);
Configuration config = Configuration.fromToml("", builder.toSchema());
// @formatter:off
String expected =
"## One two three\n"
+ "#\"Here now\" = 123\n"
+ "## Value of currency\n"
+ "#bar = 1.0\n"
+ "## Toggle switch\n"
+ "#foo = false\n"
+ "## Got milk\n"
+ "#somekey = \"somevalue\"\n";
// @formatter:on
assertEquals(expected.replace("\n", System.lineSeparator()), config.toToml());
}
}