Merge branch 'wip'

Conflicts:
	README.md
This commit is contained in:
moandji.ezana 2015-02-16 14:49:21 +02:00
commit 7e88ad0f93
56 changed files with 2395 additions and 917 deletions

View file

@ -1,5 +1,23 @@
# toml4j Changelog
## 0.4.0
### Changed
* __BREAKING:__ Toml#getList(String) replaced Toml#getList(String, Class)
* Dropped dependency on Parboiled and its significant transitive dependencies
### Added
* Support for [TOML 0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md)
* Toml#isEmpty()
* Line numbers included in error messages
### Fixed
* Short-form Unicode escape handling
* Exponent handling
## 0.3.1 / 2014-12-16
* Support for [TOML 0.3.1](https://github.com/toml-lang/toml/tree/v0.3.1) spec
* Pass TOML validator (https://github.com/BurntSushi/toml-test), which uncovered many bugs.

9
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,9 @@
Thank you for taking the time to contribute to toml4j! Here are a few guidelines to streamline the process.
* Cover new or modified functionality with unit tests
* Amend README.md as necessary
* Update the UNRELEASED section of CHANGELOG.md, as described in [keepachangelog.com](http://keepachangelog.com)
* Use 2 spaces for indentation
* Opening braces, parentheses, etc. are not on a new line
If you are unsure about how something should be implemented, open a pull request and we'll discuss it.

View file

@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2014 Moandji Ezana
Copyright (c) 2013-2015 Moandji Ezana
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View file

@ -1,8 +1,8 @@
# toml4j
toml4j is a [TOML 0.3.1](https://github.com/toml-lang/toml/tree/v0.3.1) parser for Java.
toml4j is a [TOML 0.4.0](https://github.com/toml-lang/toml/blob/master/versions/en/toml-v0.4.0.md) parser for Java.
[![Maven Central](https://img.shields.io/maven-central/v/com.moandjiezana.toml/toml4j.svg)](https://search.maven.org/#search|gav|1|g%3A%22com.moandjiezana.toml%22%20AND%20a%3A%22toml4j%22) [![Build Status](https://img.shields.io/travis/mwanji/toml4j.svg)](https://travis-ci.org/mwanji/toml4j) [![Coverage Status](https://img.shields.io/coveralls/mwanji/toml4j.svg)](https://coveralls.io/r/mwanji/toml4j)
[![Maven Central](https://img.shields.io/maven-central/v/com.moandjiezana.toml/toml4j.svg)](https://search.maven.org/#search|gav|1|g%3A%22com.moandjiezana.toml%22%20AND%20a%3A%22toml4j%22) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Build Status](https://img.shields.io/travis/mwanji/toml4j/wip.svg)](https://travis-ci.org/mwanji/toml4j) [![Coverage Status](https://img.shields.io/coveralls/mwanji/toml4j.svg)](https://coveralls.io/r/mwanji/toml4j)
For the bleeding-edge version integrating the latest specs, see the [work-in-progress branch](https://github.com/mwanji/toml4j/tree/wip).
@ -14,7 +14,7 @@ Add the following dependency to your POM (or equivalent for other dependency man
<dependency>
<groupId>com.moandjiezana.toml</groupId>
<artifactId>toml4j</artifactId>
<version>0.3.1</version>
<version>0.4.0</version>
</dependency>
```
@ -51,6 +51,9 @@ name = "Mwanji Ezana"
[address]
street = "123 A Street"
city = "AnyVille"
[contacts]
"email address" = me@example.com
```
```java
@ -62,6 +65,7 @@ class Address {
class User {
String name;
Address address;
Map<String, Object> contacts;
}
```
@ -70,18 +74,24 @@ User user = new Toml().parse(tomlFile).to(User.class);
assert user.name.equals("Mwanji Ezana");
assert user.address.street.equals("123 A Street");
assert user.contacts.get("\"email address\"").equals("me@example.com");
```
Any keys not found in both the TOML and the class are ignored. Fields may be private.
All TOML primitives can be mapped, as well as a number of Java-specific types:
Quoted keys cannot be mapped directly to a Java object, but they can be used as keys within a `Map`.
* A TOML Number can be converted to any primitive type (or the wrapper equivalent), `BigInteger` or `BigDecimal`
* A TOML string can be converted to a `String`, enum, `java.net.URI` or `java.net.URL`
* A single-letter TOML string can be converted to a `char` or `Character`
* Multiline and literal TOML strings can be converted to `String`
* A TOML array can be converted to a `List`, `Set` or array. The generic type can be anything that can be converted.
* A TOML table can be converted to a custom class or to a `Map<String, Object>`. The generic type of the value can be anything that can be converted.
TOML primitives can be mapped to a number of Java types:
TOML | Java
---- | ----
Integer | `int`, `long` (or wrapper), `java.math.BigInteger`
Float | `float`, `double` (or wrapper), `java.math.BigDecimal`
String | `String`, enum, `java.net.URI`, `java.net.URL`
One-letter String | `char`, `Character`
Multiline and Literal Strings | `String`
Array | `List`, `Set`, array. The generic type can be anything that can be converted.
Table | Custom class, `Map<String, Object>`
Custom classes, Maps and collections thereof can be nested to any level. See [TomlToClassTest#should_convert_fruit_table_array()](src/test/java/com/moandjiezana/toml/TomlToClassTest.java) for an example.
@ -94,7 +104,7 @@ Use the getters to retrieve the data:
* `getBoolean(String)`
* `getLong(String)`
* `getDouble(String)`
* `getList(String, Class<T>)`
* `getList(String)`
* `getTable(String)` returns a new Toml instance containing only the keys in that table.
* `getTables(String)`, for table arrays, returns `List<Toml>`.
@ -102,8 +112,11 @@ You can also navigate values within a table with a compound key of the form `tab
Non-existent keys return null.
When retrieving quoted keys, the quotes must be used and the key must be spelled exactly the same way, including quotes and whitespace. The only exceptions are Unicode escapes: `"\u00B1" = "value"` would be retrieved with `toml.getString("\"±\"")`.
```toml
title = "TOML Example"
"sub title" = "Now with quoted keys"
[database]
ports = [ 8001, 8001, 8002 ]
@ -136,8 +149,9 @@ title = "TOML Example"
Toml toml = new Toml().parse(getTomlFile());
String title = toml.getString("title");
String subTitle = toml.getString("\"sub title\"");
Boolean enabled = toml.getBoolean("database.enabled");
List<Long> ports = toml.getList("database.ports", Long.class);
List<Long> ports = toml.getList("database.ports");
String password = toml.getString("database.credentials.password");
Toml servers = toml.getTable("servers");
@ -192,4 +206,4 @@ Date precision is limited to milliseconds.
## License
toml4j is copyright of Moandji Ezana and is licensed under the [MIT License](LICENSE)
toml4j is copyright (c) 2013-2015 Moandji Ezana and is licensed under the [MIT License](LICENSE)

54
pom.xml
View file

@ -11,6 +11,26 @@
<name>toml4j</name>
<description>A parser for TOML</description>
<url>http://moandjiezana.com/toml/toml4j</url>
<licenses>
<license>
<name>The MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:git@github.com:mwanji/toml4j.git</connection>
<developerConnection>scm:git:git@github.com:mwanji/toml4j.git</developerConnection>
<url>https://github.com/mwanji/toml4j</url>
</scm>
<issueManagement>
<system>Github Issues</system>
<url>https://github.com/mwanji/toml4j/issues</url>
</issueManagement>
<ciManagement>
<system>Travis CI</system>
<url>https://travis-ci.org/mwanji/toml4j</url>
</ciManagement>
<developers>
<developer>
<id>moandji.ezana</id>
@ -18,7 +38,8 @@
<email>mwanji@gmail.com</email>
</developer>
</developers>
<inceptionYear>2013</inceptionYear>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
@ -27,18 +48,13 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.parboiled</groupId>
<artifactId>parboiled-java</artifactId>
<version>1.1.6</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.3</version>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.easytesting</groupId>
@ -58,7 +74,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<version>3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
@ -67,7 +83,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.0.201403182114</version>
<version>0.7.2.201409121644</version>
<executions>
<execution>
<id>prepare-agent</id>
@ -80,26 +96,10 @@
<plugin>
<groupId>org.eluder.coveralls</groupId>
<artifactId>coveralls-maven-plugin</artifactId>
<version>2.2.0</version>
<version>3.0.1</version>
</plugin>
</plugins>
</build>
<licenses>
<license>
<name>The MIT License</name>
<url>http://www.opensource.org/licenses/mit-license.php</url>
<distribution>repo</distribution>
</license>
</licenses>
<scm>
<connection>scm:git:git@github.com:mwanji/toml4j.git</connection>
<developerConnection>scm:git:git@github.com:mwanji/toml4j.git</developerConnection>
<url>https://github.com/mwanji/toml4j</url>
</scm>
<issueManagement>
<system>Github Issues</system>
<url>https://github.com/mwanji/toml4j/issues</url>
</issueManagement>
<profiles>
<profile>
<id>release-sign-artifacts</id>

View file

@ -1,67 +1,80 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
import static com.moandjiezana.toml.ValueConverters.CONVERTERS;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
class ArrayConverter implements ValueConverter {
static final ArrayConverter ARRAY_PARSER = new ArrayConverter();
private static final List<Object> INVALID_ARRAY = new ArrayList<Object>();
private static final ValueConverters VALUE_ANALYSIS = new ValueConverters();
@Override
public boolean canConvert(String s) {
return s.startsWith("[");
}
@Override
public Object convert(String s) {
List<Object> tokens = parse(parser().Array(), s);
List<Object> values = convertList(tokens);
public Object convert(String s, AtomicInteger index, Context context) {
AtomicInteger line = context.line;
int startLine = line.get();
int startIndex = index.get();
List<Object> arrayItems = new ArrayList<Object>();
boolean terminated = false;
boolean inComment = false;
Results.Errors errors = new Results.Errors();
for (int i = index.incrementAndGet(); i < s.length(); i = index.incrementAndGet()) {
if (values == INVALID_ARRAY) {
return INVALID;
}
return values;
}
private List<Object> convertList(List<Object> tokens) {
ArrayList<Object> nestedList = new ArrayList<Object>();
for (Object token : tokens) {
if (token instanceof String) {
Object converted = VALUE_ANALYSIS.convert(((String) token).trim());
if (converted == INVALID) {
return INVALID_ARRAY;
}
if (isHomogenousArray(converted, nestedList)) {
nestedList.add(converted);
char c = s.charAt(i);
if (c == '#' && !inComment) {
inComment = true;
} else if (c == '\n') {
inComment = false;
line.incrementAndGet();
} else if (inComment || Character.isWhitespace(c) || c == ',') {
continue;
} else if (c == '[') {
Object converted = convert(s, index, context);
if (converted instanceof Results.Errors) {
errors.add((Results.Errors) converted);
} else if (!isHomogenousArray(converted, arrayItems)) {
errors.heterogenous(context.identifier.getName(), line.get());
} else {
return INVALID_ARRAY;
arrayItems.add(converted);
}
} else if (token instanceof List) {
@SuppressWarnings("unchecked")
List<Object> convertedList = convertList((List<Object>) token);
if (convertedList != INVALID_ARRAY && isHomogenousArray(convertedList, nestedList)) {
nestedList.add(convertedList);
continue;
} else if (c == ']') {
terminated = true;
break;
} else {
Object converted = CONVERTERS.convert(s, index, context);
if (converted instanceof Results.Errors) {
errors.add((Results.Errors) converted);
} else if (!isHomogenousArray(converted, arrayItems)) {
errors.heterogenous(context.identifier.getName(), line.get());
} else {
return INVALID_ARRAY;
arrayItems.add(converted);
}
}
}
return nestedList;
if (!terminated) {
errors.unterminated(context.identifier.getName(), s.substring(startIndex, s.length()), startLine);
}
if (errors.hasErrors()) {
return errors;
}
return arrayItems;
}
private boolean isHomogenousArray(Object o, List<?> values) {
return values.isEmpty() || values.get(0).getClass().isAssignableFrom(o.getClass()) || o.getClass().isAssignableFrom(values.get(0).getClass());
}
private ArrayConverter() {}
}

View file

@ -1,10 +1,7 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.List;
class BooleanConverter implements ValueConverter {
@ -16,14 +13,15 @@ class BooleanConverter implements ValueConverter {
}
@Override
public Object convert(String s) {
List<String> resultValue = parse(parser().Boolean(), s);
public Object convert(String s, AtomicInteger index, Context context) {
s = s.substring(index.get());
Boolean b = s.startsWith("true") ? Boolean.TRUE : Boolean.FALSE;
if (resultValue == null) {
return INVALID;
}
int endIndex = b == Boolean.TRUE ? 4 : 5;
return Boolean.valueOf(resultValue.get(0));
index.addAndGet(endIndex - 1);
return b;
}
private BooleanConverter() {}

View file

@ -13,6 +13,15 @@ abstract class Container {
static class Table extends Container {
private final Map<String, Object> values = new HashMap<String, Object>();
final String name;
Table() {
this.name = null;
}
public Table(String name) {
this.name = name;
}
@Override
boolean accepts(String key) {
@ -68,21 +77,12 @@ abstract class Container {
@Override
void put(String key, Object value) {
if (value instanceof Container.Table) {
values.add((Container.Table) value);
return;
}
getCurrent().put(key, value);
values.add((Container.Table) value);
}
@Override
Object get(String key) {
if (values.isEmpty()) {
return null;
}
return getCurrent().get(key);
throw new UnsupportedOperationException();
}
List<Map<String, Object>> getValues() {

View file

@ -0,0 +1,19 @@
package com.moandjiezana.toml;
import java.util.concurrent.atomic.AtomicInteger;
class Context {
final Identifier identifier;
final AtomicInteger line;
final Results.Errors errors;
public Context(Identifier identifier, AtomicInteger line, Results.Errors errors) {
this.identifier = identifier;
this.line = line;
this.errors = errors;
}
public Context with(Identifier identifier) {
return new Context(identifier, line, errors);
}
}

View file

@ -1,9 +1,7 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -14,47 +12,73 @@ class DateConverter implements ValueConverter {
@Override
public boolean canConvert(String s) {
Matcher matcher = DATE_REGEX.matcher(s);
if (s.length() < 5) {
return false;
}
for (int i = 0; i < 5; i++) {
char c = s.charAt(i);
if (i < 4) {
if (!Character.isDigit(c)) {
return false;
}
} else if (c != '-') {
return false;
}
}
return matcher.matches() && ValueConverterUtils.isComment(matcher.group(4));
return true;
}
@Override
public Object convert(String s) {
public Object convert(String original, AtomicInteger index, Context context) {
StringBuilder sb = new StringBuilder();
for (int i = index.get(); i < original.length(); i = index.incrementAndGet()) {
char c = original.charAt(i);
if (Character.isDigit(c) || c == '-' || c == ':' || c == '.' || c == 'T' || c == 'Z') {
sb.append(c);
} else {
index.decrementAndGet();
break;
}
}
String s = sb.toString();
Matcher matcher = DATE_REGEX.matcher(s);
matcher.matches();
s = matcher.group(1);
if (!matcher.matches()) {
Results.Errors errors = new Results.Errors();
errors.invalidValue(context.identifier.getName(), s, context.line.get());
return errors;
}
String dateString = matcher.group(1);
String zone = matcher.group(3);
String fractionalSeconds = matcher.group(2);
String format = "yyyy-MM-dd'T'HH:mm:ss";
if (fractionalSeconds != null && !fractionalSeconds.isEmpty()) {
format += ".SSS";
s += fractionalSeconds;
dateString += fractionalSeconds;
}
format += "Z";
if ("Z".equals(zone)) {
s += "+0000";
// s = s.substring(0, 22) + s.substring(23);
dateString += "+0000";
} else if (zone.contains(":")) {
s += zone.replace(":", "");
dateString += zone.replace(":", "");
}
try {
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
dateFormat.setLenient(false);
return dateFormat.parse(s);
return dateFormat.parse(dateString);
} catch (Exception e) {
return INVALID;
Results.Errors errors = new Results.Errors();
errors.invalidValue(context.identifier.getName(), s, context.line.get());
return errors;
}
}
private DateConverter() {}
public static void main(String[] args) throws ParseException {
Pattern DATE_REGEX = Pattern.compile("(\\d{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9](?:\\.\\d*)?)(Z|(?:[+\\-]\\d{2}:\\d{2}))(.*)");
Pattern DATE_REGEX2 = Pattern.compile("(\\d{4}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]:[0-5][0-9])(Z|(?:[+\\-]\\d{2}:\\d{2}))");
System.out.println(DATE_REGEX.matcher("2011-11-11T12:32:00.999-07:00").matches());
System.out.println(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ").parse("1979-05-27T00:32:00+0000"));
}
}

View file

@ -1,21 +0,0 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
class ExponentConverter implements ValueConverter {
public static final ExponentConverter EXPONENT_PARSER = new ExponentConverter();
@Override
public boolean canConvert(String s) {
return parse(parser().Exponent(), s) != null;
}
@Override
public Object convert(String s) {
String[] exponentString = ((String) parse(parser().Exponent(), s).get(0)).split("[eE]");
return Math.pow(Double.parseDouble(exponentString[0]), Double.parseDouble(exponentString[1]));
}
}

View file

@ -1,28 +0,0 @@
package com.moandjiezana.toml;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class FloatConverter implements ValueConverter {
public static final FloatConverter FLOAT_PARSER = new FloatConverter();
private static final Pattern FLOAT_REGEX = Pattern.compile("([+-]?\\d+\\.\\d+)(.*)");
@Override
public boolean canConvert(String s) {
Matcher matcher = FLOAT_REGEX.matcher(s);
return matcher.matches() && ValueConverterUtils.isComment(matcher.group(2));
}
@Override
public Object convert(String s) {
Matcher matcher = FLOAT_REGEX.matcher(s);
matcher.matches();
return Double.valueOf(matcher.group(1));
}
private FloatConverter() {}
}

View file

@ -0,0 +1,266 @@
package com.moandjiezana.toml;
class Identifier {
static final Identifier INVALID = new Identifier("", null);
private static final String ALLOWED_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890_-";
private final String name;
private final Type type;
static Identifier from(String name, Context context) {
Type type;
boolean valid;
name = name.trim();
if (name.startsWith("[[")) {
type = Type.TABLE_ARRAY;
valid = isValidTableArray(name, context);
} else if (name.startsWith("[")) {
type = Type.TABLE;
valid = isValidTable(name, context);
} else {
type = Type.KEY;
valid = isValidKey(name, context);
}
if (!valid) {
return Identifier.INVALID;
}
return new Identifier(extractName(name), type);
}
private Identifier(String name, Type type) {
this.name = name;
this.type = type;
}
String getName() {
return name;
}
String getBareName() {
if (isKey()) {
return name;
}
if (isTable()) {
return name.substring(1, name.length() - 1);
}
return name.substring(2, name.length() - 2);
}
boolean isKey() {
return type == Type.KEY;
}
boolean isTable() {
return type == Type.TABLE;
}
boolean isTableArray() {
return type == Type.TABLE_ARRAY;
}
private static enum Type {
KEY, TABLE, TABLE_ARRAY;
}
private static String extractName(String raw) {
boolean quoted = false;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < raw.length(); i++) {
char c = raw.charAt(i);
if (c == '"' && (i == 0 || raw.charAt(i - 1) != '\\')) {
quoted = !quoted;
sb.append('"');
} else if (quoted || !Character.isWhitespace(c)) {
sb.append(c);
}
}
return StringConverter.STRING_PARSER.replaceUnicodeCharacters(sb.toString());
}
private static boolean isValidKey(String name, Context context) {
if (name.trim().isEmpty()) {
context.errors.invalidKey(name, context.line.get());
return false;
}
boolean quoted = false;
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (c == '"' && (i == 0 || name.charAt(i - 1) != '\\')) {
if (!quoted && i > 0 && name.charAt(i - 1) != '.') {
context.errors.invalidKey(name, context.line.get());
return false;
}
quoted = !quoted;
} else if (!quoted && (ALLOWED_CHARS.indexOf(c) == -1)) {
context.errors.invalidKey(name, context.line.get());
return false;
}
}
return true;
}
private static boolean isValidTable(String name, Context context) {
boolean valid = true;
if (!name.endsWith("]")) {
valid = false;
}
String trimmed = name.substring(1, name.length() - 1).trim();
if (trimmed.isEmpty() || trimmed.charAt(0) == '.' || trimmed.endsWith(".")) {
valid = false;
}
if (!valid) {
context.errors.invalidTable(name, context.line.get());
return false;
}
boolean quoted = false;
boolean dotAllowed = false;
boolean quoteAllowed = true;
boolean charAllowed = true;
for (int i = 0; i < trimmed.length(); i++) {
char c = trimmed.charAt(i);
if (!valid) {
break;
}
if (c == '"') {
if (!quoteAllowed) {
valid = false;
} else if (quoted && trimmed.charAt(i - 1) != '\\') {
charAllowed = false;
dotAllowed = true;
quoteAllowed = false;
} else if (!quoted) {
quoted = true;
quoteAllowed = true;
}
} else if (quoted) {
continue;
} else if (c == '.') {
if (dotAllowed) {
charAllowed = true;
dotAllowed = false;
quoteAllowed = true;
} else {
context.errors.emptyImplicitTable(name, context.line.get());
return false;
}
} else if (Character.isWhitespace(c)) {
char prev = trimmed.charAt(i - 1);
if (!Character.isWhitespace(prev) && prev != '.' && prev != '"') {
charAllowed = false;
dotAllowed = true;
quoteAllowed = true;
}
} else {
if (charAllowed && ALLOWED_CHARS.indexOf(c) > -1) {
charAllowed = true;
dotAllowed = true;
quoteAllowed = false;
} else {
valid = false;
}
}
}
if (!valid) {
context.errors.invalidTable(name, context.line.get());
return false;
}
return true;
}
private static boolean isValidTableArray(String line, Context context) {
boolean valid = true;
if (!line.endsWith("]]")) {
valid = false;
}
String trimmed = line.substring(2, line.length() - 2).trim();
if (trimmed.isEmpty() || trimmed.charAt(0) == '.' || trimmed.endsWith(".")) {
valid = false;
}
if (!valid) {
context.errors.invalidTableArray(line, context.line.get());
return false;
}
boolean quoted = false;
boolean dotAllowed = false;
boolean quoteAllowed = true;
boolean charAllowed = true;
for (int i = 0; i < trimmed.length(); i++) {
char c = trimmed.charAt(i);
if (!valid) {
break;
}
if (c == '"') {
if (!quoteAllowed) {
valid = false;
} else if (quoted && trimmed.charAt(i - 1) != '\\') {
charAllowed = false;
dotAllowed = true;
quoteAllowed = false;
} else if (!quoted) {
quoted = true;
quoteAllowed = true;
}
} else if (quoted) {
continue;
} else if (c == '.') {
if (dotAllowed) {
charAllowed = true;
dotAllowed = false;
quoteAllowed = true;
} else {
context.errors.emptyImplicitTable(line, context.line.get());
return false;
}
} else if (Character.isWhitespace(c)) {
char prev = trimmed.charAt(i - 1);
if (!Character.isWhitespace(prev) && prev != '.' && prev != '"') {
charAllowed = false;
dotAllowed = true;
quoteAllowed = true;
}
} else {
if (charAllowed && ALLOWED_CHARS.indexOf(c) > -1) {
charAllowed = true;
dotAllowed = true;
quoteAllowed = false;
} else {
valid = false;
}
}
}
if (!valid) {
context.errors.invalidTableArray(line, context.line.get());
return false;
}
return true;
}
}

View file

@ -0,0 +1,62 @@
package com.moandjiezana.toml;
import java.util.concurrent.atomic.AtomicInteger;
class IdentifierConverter {
static final IdentifierConverter IDENTIFIER_CONVERTER = new IdentifierConverter();
Identifier convert(String s, AtomicInteger index, Context context) {
boolean quoted = false;
StringBuilder name = new StringBuilder();
boolean terminated = false;
boolean isKey = s.charAt(index.get()) != '[';
boolean isTableArray = !isKey && s.length() > index.get() + 1 && s.charAt(index.get() + 1) == '[';
boolean inComment = false;
for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) {
char c = s.charAt(i);
if (c == '"' && (i == 0 || s.charAt(i - 1) != '\\')) {
quoted = !quoted;
name.append('"');
} else if (c == '\n') {
index.decrementAndGet();
break;
} else if (quoted) {
name.append(c);
} else if (c == '=' && isKey) {
terminated = true;
break;
} else if (c == ']' && !isKey) {
if (!isTableArray || s.length() > index.get() + 1 && s.charAt(index.get() + 1) == ']') {
terminated = true;
name.append(']');
if (isTableArray) {
name.append(']');
}
}
} else if (terminated && c == '#') {
inComment = true;
} else if (terminated && !Character.isWhitespace(c) && !inComment) {
terminated = false;
break;
} else if (!terminated) {
name.append(c);
}
}
if (!terminated) {
if (isKey) {
context.errors.unterminatedKey(name.toString(), context.line.get());
} else {
context.errors.invalidKey(name.toString(), context.line.get());
}
return Identifier.INVALID;
}
return Identifier.from(name.toString(), context);
}
private IdentifierConverter() {}
}

View file

@ -0,0 +1,77 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverters.CONVERTERS;
import java.util.HashMap;
import java.util.concurrent.atomic.AtomicInteger;
class InlineTableConverter implements ValueConverter {
static final InlineTableConverter INLINE_TABLE_PARSER = new InlineTableConverter();
@Override
public boolean canConvert(String s) {
return s.startsWith("{");
}
@Override
public Object convert(String s, AtomicInteger sharedIndex, Context context) {
AtomicInteger line = context.line;
int startLine = line.get();
int startIndex = sharedIndex.get();
boolean inKey = true;
boolean inValue = false;
boolean terminated = false;
StringBuilder currentKey = new StringBuilder();
HashMap<String, Object> results = new HashMap<String, Object>();
Results.Errors errors = new Results.Errors();
for (int i = sharedIndex.incrementAndGet(); sharedIndex.get() < s.length(); i = sharedIndex.incrementAndGet()) {
char c = s.charAt(i);
if (inValue && !Character.isWhitespace(c)) {
Object converted = CONVERTERS.convert(s, sharedIndex, context.with(Identifier.from(currentKey.toString(), context)));
if (converted instanceof Results.Errors) {
errors.add((Results.Errors) converted);
return errors;
}
String currentKeyTrimmed = currentKey.toString().trim();
Object previous = results.put(currentKeyTrimmed, converted);
if (previous != null) {
errors.duplicateKey(currentKeyTrimmed, context.line.get());
return errors;
}
currentKey = new StringBuilder();
inValue = false;
} else if (c == ',') {
inKey = true;
inValue = false;
currentKey = new StringBuilder();
} else if (c == '=') {
inKey = false;
inValue = true;
} else if (c == '}') {
terminated = true;
break;
} else if (inKey) {
currentKey.append(c);
}
}
if (!terminated) {
errors.unterminated(context.identifier.getName(), s.substring(startIndex), startLine);
}
if (errors.hasErrors()) {
return errors;
}
return results;
}
private InlineTableConverter() {}
}

View file

@ -1,29 +0,0 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
import java.util.List;
class IntegerConverter implements ValueConverter {
static final IntegerConverter INTEGER_PARSER = new IntegerConverter();
@Override
public boolean canConvert(String s) {
return parse(parser().Integer(), s) != null;
}
@Override
public Object convert(String s) {
List<String> resultValue = parse(parser().Integer(), s);
String longString = resultValue.get(0);
if (longString.startsWith("+")) {
longString = longString.substring(1);
}
return Long.valueOf(longString);
}
private IntegerConverter() {}
}

View file

@ -0,0 +1,65 @@
package com.moandjiezana.toml;
import java.util.ArrayList;
import java.util.List;
class Keys {
static class Key {
final String name;
final int index;
final String path;
Key(String name, int index, Key next) {
this.name = name;
this.index = index;
if (next != null) {
this.path = name + "." + next.path;
} else {
this.path = name;
}
}
}
static Keys.Key[] split(String key) {
List<Key> splitKey = new ArrayList<Key>();
StringBuilder current = new StringBuilder();
boolean quoted = false;
boolean indexable = true;
boolean inIndex = false;
int index = -1;
for (int i = key.length() - 1; i > -1; i--) {
char c = key.charAt(i);
if (c == ']' && indexable) {
inIndex = true;
continue;
}
indexable = false;
if (c == '[' && inIndex) {
inIndex = false;
index = Integer.parseInt(current.toString());
current = new StringBuilder();
continue;
}
if (c == '"' && (i == 0 || key.charAt(i - 1) != '\\')) {
quoted = !quoted;
indexable = false;
}
if (c != '.' || quoted) {
current.insert(0, c);
} else {
splitKey.add(0, new Key(current.toString(), index, !splitKey.isEmpty() ? splitKey.get(0) : null));
indexable = true;
index = -1;
current = new StringBuilder();
}
}
splitKey.add(0, new Key(current.toString(), index, !splitKey.isEmpty() ? splitKey.get(0) : null));
return splitKey.toArray(new Key[0]);
}
private Keys() {}
}

View file

@ -1,9 +1,7 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.List;
class LiteralStringConverter implements ValueConverter {
@ -15,14 +13,29 @@ class LiteralStringConverter implements ValueConverter {
}
@Override
public Object convert(String s) {
List<String> resultValue = parse(parser().LiteralString(), s);
public Object convert(String s, AtomicInteger index, Context context) {
int startLine = context.line.get();
boolean terminated = false;
int startIndex = index.incrementAndGet();
if (resultValue == null) {
return ValueConverterUtils.INVALID;
for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) {
char c = s.charAt(i);
if (c == '\'') {
terminated = true;
break;
}
}
return resultValue.get(0);
if (!terminated) {
Results.Errors errors = new Results.Errors();
errors.unterminated(context.identifier.getName(), s.substring(startIndex), startLine);
return errors;
}
String substring = s.substring(startIndex, index.get());
return substring;
}
private LiteralStringConverter() {}

View file

@ -1,30 +1,9 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
import java.util.List;
import org.parboiled.errors.ParseError;
import org.parboiled.parserunners.RecoveringParseRunner;
import org.parboiled.support.ParseTreeUtils;
import org.parboiled.support.ParsingResult;
import java.util.concurrent.atomic.AtomicInteger;
class MultilineLiteralStringConverter implements ValueConverter {
public static void main(String[] args) {
ParsingResult<List<java.lang.String>> parsingResult = new RecoveringParseRunner<List<String>>(ValueConverterUtils.parser().MultilineLiteralString()).run("'''abc''' # comment");
if (parsingResult.hasErrors()) {
for (ParseError parseError : parsingResult.parseErrors) {
System.out.println(parseError.getInputBuffer().extract(0, 1000));
}
}
System.out.println(ParseTreeUtils.printNodeTree(parsingResult));
}
static final MultilineLiteralStringConverter MULTILINE_LITERAL_STRING_CONVERTER = new MultilineLiteralStringConverter();
@Override
@ -33,14 +12,39 @@ class MultilineLiteralStringConverter implements ValueConverter {
}
@Override
public Object convert(String s) {
List<String> result = parse(parser().MultilineLiteralString(), s);
public Object convert(String s, AtomicInteger index, Context context) {
AtomicInteger line = context.line;
int startLine = line.get();
int originalStartIndex = index.get();
int startIndex = index.addAndGet(3);
int endIndex = -1;
if (result == null) {
return INVALID;
if (s.charAt(startIndex) == '\n') {
startIndex = index.incrementAndGet();
line.incrementAndGet();
}
return result.get(0);
for (int i = startIndex; i < s.length(); i = index.incrementAndGet()) {
char c = s.charAt(i);
if (c == '\n') {
line.incrementAndGet();
}
if (c == '\'' && s.length() > i + 2 && s.charAt(i + 1) == '\'' && s.charAt(i + 2) == '\'') {
endIndex = i;
index.addAndGet(2);
break;
}
}
if (endIndex == -1) {
Results.Errors errors = new Results.Errors();
errors.unterminated(context.identifier.getName(), s.substring(originalStartIndex), startLine);
return errors;
}
return s.substring(startIndex, endIndex);
}
private MultilineLiteralStringConverter() {}

View file

@ -1,9 +1,9 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import java.util.concurrent.atomic.AtomicInteger;
class MultilineStringConverter implements ValueConverter {
static final MultilineStringConverter MULTILINE_STRING_PARSER = new MultilineStringConverter();
@Override
@ -12,23 +12,45 @@ class MultilineStringConverter implements ValueConverter {
}
@Override
public Object convert(String s) {
int terminator = s.indexOf("\"\"\"", 3);
public Object convert(String s, AtomicInteger index, Context context) {
AtomicInteger line = context.line;
int startLine = line.get();
int originalStartIndex = index.get();
int startIndex = index.addAndGet(3);
int endIndex = -1;
if (terminator == -1) {
return INVALID;
if (s.charAt(startIndex) == '\n') {
startIndex = index.incrementAndGet();
line.incrementAndGet();
}
if (!ValueConverterUtils.isComment(s.substring(terminator + 3))) {
return INVALID;
for (int i = startIndex; i < s.length(); i = index.incrementAndGet()) {
char c = s.charAt(i);
if (c == '\n') {
line.incrementAndGet();
} else if (c == '"' && s.length() > i + 2 && s.charAt(i + 1) == '"' && s.charAt(i + 2) == '"') {
endIndex = i;
index.addAndGet(2);
break;
}
}
s = s.substring(2, terminator + 1);
if (endIndex == -1) {
Results.Errors errors = new Results.Errors();
errors.unterminated(context.identifier.getName(), s.substring(originalStartIndex), startLine);
return errors;
}
s = s.substring(startIndex, endIndex);
s = s.replaceAll("\\\\\\s+", "");
return StringConverter.STRING_PARSER.convert(s);
s = StringConverter.STRING_PARSER.replaceUnicodeCharacters(s);
s = StringConverter.STRING_PARSER.replaceSpecialCharacters(s);
return s;
}
private MultilineStringConverter() {
}
private MultilineStringConverter() {}
}

View file

@ -0,0 +1,78 @@
package com.moandjiezana.toml;
import java.util.concurrent.atomic.AtomicInteger;
class NumberConverter implements ValueConverter {
static final NumberConverter NUMBER_PARSER = new NumberConverter();
@Override
public boolean canConvert(String s) {
char firstChar = s.charAt(0);
return firstChar == '+' || firstChar == '-' || Character.isDigit(firstChar);
}
@Override
public Object convert(String s, AtomicInteger index, Context context) {
boolean signable = true;
boolean dottable = false;
boolean exponentable = false;
boolean terminatable = false;
String type = "";
StringBuilder sb = new StringBuilder();
for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
sb.append(c);
signable = false;
terminatable = true;
if (type.isEmpty()) {
type = "integer";
dottable = true;
}
exponentable = !type.equals("exponent");
} else if ((c == '+' || c == '-') && signable && s.length() > i + 1) {
signable = false;
terminatable = false;
if (c == '-') {
sb.append('-');
}
} else if (c == '.' && dottable && s.length() > i + 1) {
sb.append('.');
type = "float";
terminatable = false;
dottable = false;
exponentable = false;
} else if ((c == 'E' || c == 'e') && exponentable && s.length() > i + 1) {
sb.append('E');
type = "exponent";
terminatable = false;
signable = true;
dottable = false;
exponentable = false;
} else {
if (!terminatable) {
type = "";
}
index.decrementAndGet();
break;
}
}
if (type.equals("integer")) {
return Long.valueOf(sb.toString());
} else if (type.equals("float")) {
return Double.valueOf(sb.toString());
} else if (type.equals("exponent")) {
String[] exponentString = sb.toString().split("E");
return Double.parseDouble(exponentString[0]) * Math.pow(10, Double.parseDouble(exponentString[1]));
} else {
Results.Errors errors = new Results.Errors();
errors.invalidValue(context.identifier.getName(), sb.toString(), context.line.get());
return errors;
}
}
}

View file

@ -3,35 +3,166 @@ package com.moandjiezana.toml;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
class Results {
Set<String> tables = new HashSet<String>();
StringBuilder errors = new StringBuilder();
private Deque<Container> stack = new ArrayDeque<Container>();
static class Errors {
private final StringBuilder sb = new StringBuilder();
void duplicateTable(String table, int line) {
sb.append("Duplicate table definition on line ")
.append(line)
.append(": [")
.append(table)
.append("]\n");
}
void emptyImplicitTable(String table, int line) {
sb.append("Invalid table definition due to empty implicit table name: ")
.append(table)
.append("\n");
}
void invalidTable(String table, int line) {
sb.append("Invalid table definition on line ")
.append(line)
.append(": ")
.append(table)
.append("]\n");
}
void duplicateKey(String key, int line) {
sb.append("Duplicate key");
if (line > -1) {
sb.append(" on line ")
.append(line);
}
sb.append(": ")
.append(key)
.append('\n');
}
void invalidTextAfterIdentifier(Identifier identifier, char text, int line) {
sb.append("Invalid text after key ")
.append(identifier.getName())
.append(" on line ")
.append(line)
.append(". Make sure to terminate the value or add a comment (#).")
.append('\n');
}
void invalidKey(String key, int line) {
sb.append("Invalid key on line ")
.append(line)
.append(": ")
.append(key)
.append('\n');
}
void invalidTableArray(String tableArray, int line) {
sb.append("Invalid table array definition on line ")
.append(line)
.append(": ")
.append(tableArray)
.append('\n');
}
void invalidValue(String key, String value, int line) {
sb.append("Invalid value on line ")
.append(line)
.append(": ")
.append(key)
.append(" = ")
.append(value)
.append('\n');
}
void unterminatedKey(String key, int line) {
sb.append("Key is not followed by an equals sign on line ")
.append(line)
.append(": ")
.append(key)
.append('\n');
}
void unterminated(String key, String value, int line) {
sb.append("Unterminated value on line ")
.append(line)
.append(": ")
.append(key)
.append(" = ")
.append(value.trim())
.append('\n');
}
public void heterogenous(String key, int line) {
sb.append(key)
.append(" becomes a heterogeneous array on line ")
.append(line)
.append('\n');
}
boolean hasErrors() {
return sb.length() > 0;
}
@Override
public String toString() {
return sb.toString();
}
public void add(Errors other) {
sb.append(other.sb);
}
}
final Errors errors = new Errors();
private final Set<String> tables = new HashSet<String>();
private final Deque<Container> stack = new ArrayDeque<Container>();
Results() {
stack.push(new Container.Table());
stack.push(new Container.Table(""));
}
void addValue(String key, Object value) {
void addValue(String key, Object value, AtomicInteger line) {
Container currentTable = stack.peek();
if (currentTable.accepts(key)) {
if (value instanceof Map) {
String path = getInlineTablePath(key);
if (path == null) {
startTable(key, line);
} else if (path.isEmpty()) {
startTables(Identifier.from(key, null), line);
} else {
startTables(Identifier.from(path, null), line);
}
@SuppressWarnings("unchecked")
Map<String, Object> valueMap = (Map<String, Object>) value;
for (Map.Entry<String, Object> entry : valueMap.entrySet()) {
addValue(entry.getKey(), entry.getValue(), line);
}
stack.pop();
} else if (currentTable.accepts(key)) {
currentTable.put(key, value);
} else {
errors.append("Key " + key + " is defined twice!\n");
errors.duplicateKey(key, line != null ? line.get() : -1);
}
}
void startTableArray(String tableName) {
void startTableArray(Identifier identifier, AtomicInteger line) {
String tableName = identifier.getBareName();
while (stack.size() > 1) {
stack.pop();
}
String[] tableParts = tableName.split("\\.");
Keys.Key[] tableParts = Keys.split(tableName);
for (int i = 0; i < tableParts.length; i++) {
String tablePart = tableParts[i];
String tablePart = tableParts[i].name;
Container currentContainer = stack.peek();
if (currentContainer.get(tablePart) instanceof Container.TableArray) {
@ -49,48 +180,43 @@ class Results {
stack.push(nextTable);
} else if (currentContainer.accepts(tablePart)) {
Container newContainer = i == tableParts.length - 1 ? new Container.TableArray() : new Container.Table();
addValue(tablePart, newContainer);
addValue(tablePart, newContainer, line);
stack.push(newContainer);
if (newContainer instanceof Container.TableArray) {
stack.push(((Container.TableArray) newContainer).getCurrent());
}
} else {
errors.append("Duplicate key and table definitions for " + tableName + "!\n");
errors.duplicateTable(tableName, line.get());
break;
}
}
}
void startTables(String tableName) {
void startTables(Identifier id, AtomicInteger line) {
String tableName = id.getBareName();
if (!tables.add(tableName)) {
errors.append("Table " + tableName + " defined twice!\n");
errors.duplicateTable(tableName, line.get());
}
if (tableName.endsWith(".")) {
errors.append("Implicit table name cannot be empty: " + tableName);
}
while (stack.size() > 1) {
stack.pop();
}
String[] tableParts = tableName.split("\\.");
Keys.Key[] tableParts = Keys.split(tableName);
for (int i = 0; i < tableParts.length; i++) {
String tablePart = tableParts[i];
String tablePart = tableParts[i].name;
Container currentContainer = stack.peek();
if (tablePart.isEmpty()) {
errors.append("Empty implicit table: " + tableName + "!\n");
} else if (currentContainer.get(tablePart) instanceof Container) {
if (currentContainer.get(tablePart) instanceof Container) {
Container nextTable = (Container) currentContainer.get(tablePart);
stack.push(nextTable);
if (stack.peek() instanceof Container.TableArray) {
stack.push(((Container.TableArray) stack.peek()).getCurrent());
}
} else if (currentContainer.accepts(tablePart)) {
startTable(tablePart);
startTable(tablePart, line);
} else {
errors.append("Duplicate key and table definitions for " + tableName + "!\n");
errors.duplicateTable(tableName, -1);
break;
}
}
@ -106,11 +232,45 @@ class Results {
return ((Container.Table) values).consume();
}
private Container startTable(String tableName) {
Container newTable = new Container.Table();
addValue(tableName, newTable);
private Container startTable(String tableName, AtomicInteger line) {
Container newTable = new Container.Table(tableName);
addValue(tableName, newTable, line);
stack.push(newTable);
return newTable;
}
private String getInlineTablePath(String key) {
Iterator<Container> descendingIterator = stack.descendingIterator();
StringBuilder sb = new StringBuilder();
while (descendingIterator.hasNext()) {
Container next = descendingIterator.next();
if (next instanceof Container.TableArray) {
return null;
}
Container.Table table = (Container.Table) next;
if (table.name == null) {
break;
}
if (sb.length() > 0) {
sb.append('.');
}
sb.append(table.name);
}
if (sb.length() > 0) {
sb.append('.');
}
sb.append(key)
.insert(0, '[')
.append(']');
return sb.toString();
}
}

View file

@ -1,15 +1,13 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import static com.moandjiezana.toml.ValueConverterUtils.isComment;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class StringConverter implements ValueConverter {
static final StringConverter STRING_PARSER = new StringConverter();
private static final Pattern UNICODE_REGEX = Pattern.compile("\\\\[uU](.*)");
private static final Pattern UNICODE_REGEX = Pattern.compile("\\\\[uU](.{4})");
@Override
public boolean canConvert(String s) {
@ -17,43 +15,38 @@ class StringConverter implements ValueConverter {
}
@Override
public Object convert(String value) {
int stringTerminator = -1;
char[] chars = value.toCharArray();
public Object convert(String s, AtomicInteger index, Context context) {
int startIndex = index.incrementAndGet();
int endIndex = -1;
for (int i = 1; i < chars.length; i++) {
char ch = chars[i];
if (ch == '"' && chars[i - 1] != '\\') {
stringTerminator = i;
for (int i = index.get(); i < s.length(); i = index.incrementAndGet()) {
char ch = s.charAt(i);
if (ch == '"' && s.charAt(i - 1) != '\\') {
endIndex = i;
break;
}
}
if (stringTerminator == -1 || !isComment(value.substring(stringTerminator + 1))) {
return INVALID;
if (endIndex == -1) {
Results.Errors errors = new Results.Errors();
errors.unterminated(context.identifier.getName(), s.substring(startIndex - 1), context.line.get());
return errors;
}
value = value.substring(1, stringTerminator);
value = replaceUnicodeCharacters(value);
chars = value.toCharArray();
for (int i = 0; i < chars.length - 1; i++) {
char ch = chars[i];
char next = chars[i + 1];
if (ch == '\\' && next == '\\') {
i++;
} else if (ch == '\\' && !(next == 'b' || next == 'f' || next == 'n' || next == 't' || next == 'r' || next == '"' || next == '/' || next == '\\')) {
return INVALID;
}
String raw = s.substring(startIndex, endIndex);
s = replaceUnicodeCharacters(raw);
s = replaceSpecialCharacters(s);
if (s == null) {
Results.Errors errors = new Results.Errors();
errors.invalidValue(context.identifier.getName(), raw, context.line.get());
return errors;
}
value = replaceSpecialCharacters(value);
return value;
return s;
}
private String replaceUnicodeCharacters(String value) {
String replaceUnicodeCharacters(String value) {
Matcher unicodeMatcher = UNICODE_REGEX.matcher(value);
while (unicodeMatcher.find()) {
@ -62,8 +55,19 @@ class StringConverter implements ValueConverter {
return value;
}
private String replaceSpecialCharacters(String value) {
return value.replace("\\n", "\n")
String replaceSpecialCharacters(String s) {
for (int i = 0; i < s.length() - 1; i++) {
char ch = s.charAt(i);
char next = s.charAt(i + 1);
if (ch == '\\' && next == '\\') {
i++;
} else if (ch == '\\' && !(next == 'b' || next == 'f' || next == 'n' || next == 't' || next == 'r' || next == '"' || next == '\\')) {
return null;
}
}
return s.replace("\\n", "\n")
.replace("\\\"", "\"")
.replace("\\t", "\t")
.replace("\\r", "\r")

View file

@ -8,19 +8,13 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
@ -28,9 +22,11 @@ import com.google.gson.JsonElement;
/**
* <p>Provides access to the keys and tables in a TOML data source.</p>
*
* <p>All getters can fall back to default values if they have been provided.
* <p>All getters can fall back to default values if they have been provided as a constructor argument.
* Getters for simple values (String, Date, etc.) will return null if no matching key exists.
* {@link #getList(String, Class)}, {@link #getTable(String)} and {@link #getTables(String)} return empty values if there is no matching key.</p>
* {@link #getList(String)}, {@link #getTable(String)} and {@link #getTables(String)} return empty values if there is no matching key.</p>
*
* <p>All parse methods throw an {@link IllegalStateException} if the TOML is incorrect.</p>
*
* <p>Example usage:</p>
* <pre><code>
@ -45,7 +41,6 @@ import com.google.gson.JsonElement;
public class Toml {
private static final Gson DEFAULT_GSON = new Gson();
private static final Pattern ARRAY_INDEX_PATTERN = Pattern.compile("(.*)\\[(\\d+)\\]");
private Map<String, Object> values = new HashMap<String, Object>();
private final Toml defaults;
@ -127,8 +122,8 @@ public class Toml {
* @throws IllegalStateException If tomlString is not valid TOML
*/
public Toml parse(String tomlString) throws IllegalStateException {
Results results = new TomlParser().run(tomlString);
if (results.errors.length() > 0) {
Results results = TomlParser.run(tomlString);
if (results.errors.hasErrors()) {
throw new IllegalStateException(results.errors.toString());
}
@ -145,8 +140,13 @@ public class Toml {
return (Long) get(key);
}
@SuppressWarnings("unchecked")
public <T> List<T> getList(String key, Class<T> itemClass) {
/**
* @param key a TOML key
* @param <T> type of list items
* @return an empty {@link List} is the key is not found
*/
public <T> List<T> getList(String key) {
@SuppressWarnings("unchecked")
List<T> list = (List<T>) get(key);
if (list == null) {
@ -179,7 +179,7 @@ public class Toml {
/**
* @param key Name of array of tables, not including square brackets.
* @return An empty List if no value is found for key.
* @return An empty {@link List} if no value is found for key.
*/
@SuppressWarnings("unchecked")
public List<Toml> getTables(String key) {
@ -198,19 +198,29 @@ public class Toml {
return tables;
}
public boolean isEmpty() {
return values.isEmpty();
}
/**
* <p>Populates an instance of targetClass with the values of this Toml instance.
* The target's field names must match keys or tables.
* Keys not present in targetClass will be ignored.</p>
* <p>
* Populates an instance of targetClass with the values of this Toml instance.
* The target's field names must match keys or tables.
* Keys not present in targetClass will be ignored.
* </p>
*
* <p>Tables are recursively converted to custom classes or to {@link Map Map&lt;String, Object&gt;}.</p>
*
* <p>In addition to straight-forward conversion of TOML primitives, the following are also available:</p>
*
* <ul>
* <li>TOML string to {@link Character}, {@link URL} or enum</li>
* <li>TOML number to any primitive (or wrapper), {@link BigInteger} or {@link BigDecimal}</li>
* <li>TOML array to {@link Set}</li>
* <li>Integer -&gt; int, long (or wrapper), {@link java.math.BigInteger}</li>
* <li>Float -&gt; float, double (or wrapper), {@link java.math.BigDecimal}</li>
* <li>One-letter String -&gt; char, {@link Character}</li>
* <li>String -&gt; {@link String}, enum, {@link java.net.URI}, {@link java.net.URL}</li>
* <li>Multiline and Literal Strings -&gt; {@link String}</li>
* <li>Array -&gt; {@link List}, {@link Set}, array. The generic type can be anything that can be converted.</li>
* <li>Table -&gt; Custom class, {@link Map Map&lt;String, Object&gt;}</li>
* </ul>
*
* @param targetClass Class to deserialize TOML to.
@ -246,55 +256,32 @@ public class Toml {
@SuppressWarnings("unchecked")
private Object get(String key) {
String[] split = key.split("\\.");
if (values.containsKey(key)) {
return values.get(key);
}
Object current = new HashMap<String, Object>(values);
for (int i = 0; i < split.length; i++) {
if (i == 0 && values.containsKey(key)) {
return values.get(key);
}
String keyWithDot = join(Arrays.copyOfRange(split, i, split.length));
if (current instanceof Map && ((Map<String, Object>) current).containsKey(keyWithDot)) {
return ((Map<String, Object>) current).get(keyWithDot);
}
String splitKey = split[i];
Matcher matcher = ARRAY_INDEX_PATTERN.matcher(splitKey);
int index = -1;
if (matcher.find()) {
splitKey = matcher.group(1);
index = Integer.parseInt(matcher.group(2), 10);
Keys.Key[] keys = Keys.split(key);
for (Keys.Key k : keys) {
if (k.index == -1 && current instanceof Map && ((Map<String, Object>) current).containsKey(k.path)) {
return ((Map<String, Object>) current).get(k.path);
}
current = ((Map<String, Object>) current).get(splitKey);
current = ((Map<String, Object>) current).get(k.name);
if (index > -1 && current != null) {
current = ((List<?>) current).get(index);
if (k.index > -1 && current != null) {
current = ((List<?>) current).get(k.index);
}
if (current == null) {
return defaults != null ? defaults.get(key) : null;
}
}
return current;
}
private String join(String[] strings) {
StringBuilder sb = new StringBuilder();
for (String string : strings) {
sb.append(string).append('.');
}
if (sb.length() > 0) {
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
private Toml(Toml defaults, Map<String, Object> values) {
this.values = values != null ? values : Collections.<String, Object>emptyMap();

View file

@ -1,251 +1,65 @@
package com.moandjiezana.toml;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import static com.moandjiezana.toml.ValueConverterUtils.parse;
import static com.moandjiezana.toml.ValueConverterUtils.parser;
import static com.moandjiezana.toml.IdentifierConverter.IDENTIFIER_CONVERTER;
import java.util.List;
import java.util.regex.Pattern;
import java.util.concurrent.atomic.AtomicInteger;
class TomlParser {
private static final String STRING_LITERAL_DELIMITER = "'''";
private static final Pattern MULTILINE_ARRAY_REGEX = Pattern.compile("\\s*\\[([^\\]]*)");
private static final Pattern MULTILINE_ARRAY_REGEX_END = Pattern.compile("\\s*\\]");
private static final ValueConverters VALUE_ANALYSIS = new ValueConverters();
private final Results results = new Results();
Results run(String tomlString) {
static Results run(String tomlString) {
final Results results = new Results();
if (tomlString.isEmpty()) {
return results;
}
String[] lines = tomlString.split("[\\n\\r]");
StringBuilder multilineBuilder = new StringBuilder();
Multiline multiline = Multiline.NONE;
String key = null;
String value = null;
for (int i = 0; i < lines.length; i++) {
String line = lines[i];
if (line != null && multiline.isTrimmable()) {
line = line.trim();
}
if (isComment(line) || line.isEmpty()) {
continue;
}
if (isTableArray(line)) {
String tableName = getTableArrayName(line);
if (tableName != null) {
results.startTableArray(tableName);
String afterTableName = line.substring(tableName.length() + 4);
if (!isComment(afterTableName)) {
results.errors.append("Invalid table array definition: " + line + "\n\n");
}
} else {
results.errors.append("Invalid table array definition: " + line + "\n\n");
}
continue;
}
if (multiline.isNotMultiline() && isTable(line)) {
String tableName = getTableName(line);
if (tableName != null) {
results.startTables(tableName);
} else {
results.errors.append("Invalid table definition: " + line + "\n\n");
}
continue;
}
if (multiline.isNotMultiline() && !line.contains("=")) {
results.errors.append("Invalid key definition: " + line);
continue;
}
String[] pair = line.split("=", 2);
if (multiline.isNotMultiline() && MULTILINE_ARRAY_REGEX.matcher(pair[1].trim()).matches()) {
multiline = Multiline.ARRAY;
key = pair[0].trim();
multilineBuilder.append(removeComment(pair[1]));
continue;
}
if (multiline.isNotMultiline() && pair[1].trim().startsWith("\"\"\"")) {
multiline = Multiline.STRING;
multilineBuilder.append(pair[1]);
key = pair[0].trim();
if (pair[1].trim().indexOf("\"\"\"", 3) > -1) {
multiline = Multiline.NONE;
pair[1] = multilineBuilder.toString().trim();
multilineBuilder.delete(0, multilineBuilder.length());
} else {
if (multilineBuilder.toString().trim().length() > 3) {
multilineBuilder.append('\n');
}
continue;
}
}
if (multiline.isNotMultiline() && pair[1].trim().startsWith(STRING_LITERAL_DELIMITER)) {
multiline = Multiline.STRING_LITERAL;
multilineBuilder.append(pair[1]);
key = pair[0].trim();
if (pair[1].trim().indexOf(STRING_LITERAL_DELIMITER, 3) > -1) {
multiline = Multiline.NONE;
pair[1] = multilineBuilder.toString().trim();
multilineBuilder.delete(0, multilineBuilder.length());
} else {
if (multilineBuilder.toString().trim().length() > 3) {
multilineBuilder.append('\n');
}
continue;
}
}
if (multiline == Multiline.ARRAY) {
String lineWithoutComment = removeComment(line);
multilineBuilder.append(lineWithoutComment);
if (MULTILINE_ARRAY_REGEX_END.matcher(lineWithoutComment).matches()) {
multiline = Multiline.NONE;
value = multilineBuilder.toString();
multilineBuilder.delete(0, multilineBuilder.length());
} else {
continue;
}
} else if (multiline == Multiline.STRING) {
multilineBuilder.append(line);
if (line.contains("\"\"\"")) {
multiline = Multiline.NONE;
value = multilineBuilder.toString().trim();
multilineBuilder.delete(0, multilineBuilder.length());
} else {
multilineBuilder.append('\n');
continue;
}
} else if (multiline == Multiline.STRING_LITERAL) {
multilineBuilder.append(line);
if (line.contains(STRING_LITERAL_DELIMITER)) {
multiline = Multiline.NONE;
value = multilineBuilder.toString().trim();
multilineBuilder.delete(0, multilineBuilder.length());
} else {
multilineBuilder.append('\n');
continue;
}
} else {
key = pair[0].trim();
value = pair[1].trim();
}
if (!isKeyValid(key)) {
results.errors.append("Invalid key name: " + key + "\n");
continue;
}
Object convertedValue = VALUE_ANALYSIS.convert(value);
if (convertedValue != INVALID) {
results.addValue(key, convertedValue);
} else {
results.errors.append("Invalid key/value: " + key + " = " + value + "\n");
}
}
AtomicInteger index = new AtomicInteger();
boolean inComment = false;
AtomicInteger line = new AtomicInteger(1);
Identifier identifier = null;
Object value = null;
if (multiline != Multiline.NONE) {
results.errors.append("Unterminated multiline " + multiline.toString().toLowerCase().replace('_', ' ') + "\n");
for (int i = index.get(); i < tomlString.length(); i = index.incrementAndGet()) {
char c = tomlString.charAt(i);
if (results.errors.hasErrors()) {
break;
}
if (c == '#' && !inComment) {
inComment = true;
} else if (!Character.isWhitespace(c) && !inComment && identifier == null) {
Identifier id = IDENTIFIER_CONVERTER.convert(tomlString, index, new Context(null, line, results.errors));
if (id != Identifier.INVALID) {
if (id.isKey()) {
identifier = id;
} else if (id.isTable()) {
results.startTables(id, line);
} else if (id.isTableArray()) {
results.startTableArray(id, line);
}
}
} else if (c == '\n') {
inComment = false;
identifier = null;
value = null;
line.incrementAndGet();
} else if (!inComment && identifier != null && identifier.isKey() && value == null && !Character.isWhitespace(c)) {
value = ValueConverters.CONVERTERS.convert(tomlString, index, new Context(identifier, line, results.errors));
if (value instanceof Results.Errors) {
results.errors.add((Results.Errors) value);
} else {
results.addValue(identifier.getName(), value, line);
}
} else if (value != null && !inComment && !Character.isWhitespace(c)) {
results.errors.invalidTextAfterIdentifier(identifier, c, line.get());
}
}
return results;
}
private boolean isTableArray(String line) {
return line.startsWith("[[");
}
private String getTableArrayName(String line) {
List<Object> resultValue = parse(parser().TableArray(), line);
if (resultValue == null) {
return null;
}
return (String) resultValue.get(0);
}
private boolean isTable(String line) {
return line.startsWith("[");
}
private String getTableName(String line) {
List<Object> resultValue = parse(parser().Table(), line);
if (resultValue == null) {
return null;
}
return (String) resultValue.get(0);
}
private boolean isKeyValid(String key) {
if (key.contains("#") || key.trim().isEmpty()) {
return false;
}
return true;
}
private boolean isComment(String line) {
if (line == null || line.isEmpty()) {
return true;
}
char[] chars = line.toCharArray();
for (char c : chars) {
if (Character.isWhitespace(c)) {
continue;
}
return c == '#';
}
return false;
}
private String removeComment(String line) {
line = line.trim();
if (line.startsWith("\"")) {
int startOfComment = line.indexOf('#', line.lastIndexOf('"'));
if (startOfComment > -1) {
return line.substring(0, startOfComment - 1).trim();
}
} else {
int startOfComment = line.indexOf('#');
if (startOfComment > -1) {
return line.substring(0, startOfComment - 1).trim();
}
}
return line;
}
private static enum Multiline {
NONE, ARRAY, STRING, STRING_LITERAL;
public boolean isNotMultiline() {
return this == NONE;
}
public boolean isTrimmable() {
return this == NONE || this == ARRAY;
}
}
private TomlParser() {}
}

View file

@ -1,5 +1,7 @@
package com.moandjiezana.toml;
import java.util.concurrent.atomic.AtomicInteger;
interface ValueConverter {
/**
@ -8,7 +10,12 @@ interface ValueConverter {
boolean canConvert(String s);
/**
* @param s must already have been validated by {@link #canConvert(String)}
* Partial validation. Stops after type terminator, rather than at EOI.
*
* @param s must already have been validated by {@link #canConvert(String)}
* @param index where to start in s
* @param line current line number, used for error reporting
* @return a value or a {@link Results.Errors}
*/
Object convert(String s);
Object convert(String s, AtomicInteger index, Context context);
}

View file

@ -1,39 +0,0 @@
package com.moandjiezana.toml;
import java.util.List;
import org.parboiled.Parboiled;
import org.parboiled.Rule;
import org.parboiled.parserunners.BasicParseRunner;
class ValueConverterUtils {
static final Object INVALID = new Object();
static ValueParser parser() {
return Parboiled.createParser(ValueParser.class);
}
static <T> List<T> parse(Rule rule, String s) {
return new BasicParseRunner<List<T>>(rule).run(s).resultValue;
}
static boolean isComment(String line) {
if (line == null || line.isEmpty()) {
return true;
}
char[] chars = line.toCharArray();
for (char c : chars) {
if (Character.isWhitespace(c)) {
continue;
}
return c == '#';
}
return false;
}
private ValueConverterUtils() {}
}

View file

@ -3,28 +3,35 @@ package com.moandjiezana.toml;
import static com.moandjiezana.toml.ArrayConverter.ARRAY_PARSER;
import static com.moandjiezana.toml.BooleanConverter.BOOLEAN_PARSER;
import static com.moandjiezana.toml.DateConverter.DATE_PARSER;
import static com.moandjiezana.toml.ExponentConverter.EXPONENT_PARSER;
import static com.moandjiezana.toml.FloatConverter.FLOAT_PARSER;
import static com.moandjiezana.toml.IntegerConverter.INTEGER_PARSER;
import static com.moandjiezana.toml.InlineTableConverter.INLINE_TABLE_PARSER;
import static com.moandjiezana.toml.LiteralStringConverter.LITERAL_STRING_PARSER;
import static com.moandjiezana.toml.MultilineLiteralStringConverter.MULTILINE_LITERAL_STRING_CONVERTER;
import static com.moandjiezana.toml.MultilineStringConverter.MULTILINE_STRING_PARSER;
import static com.moandjiezana.toml.NumberConverter.NUMBER_PARSER;
import static com.moandjiezana.toml.StringConverter.STRING_PARSER;
import static com.moandjiezana.toml.ValueConverterUtils.INVALID;
import java.util.concurrent.atomic.AtomicInteger;
class ValueConverters {
private static final ValueConverter[] PARSERS = {
MULTILINE_STRING_PARSER, MULTILINE_LITERAL_STRING_CONVERTER, LITERAL_STRING_PARSER, STRING_PARSER, DATE_PARSER, EXPONENT_PARSER, INTEGER_PARSER, FLOAT_PARSER, BOOLEAN_PARSER, ARRAY_PARSER
};
public Object convert(String value) {
static final ValueConverters CONVERTERS = new ValueConverters();
Object convert(String value, AtomicInteger index, Context context) {
String substring = value.substring(index.get());
for (ValueConverter valueParser : PARSERS) {
if (valueParser.canConvert(value)) {
return valueParser.convert(value);
if (valueParser.canConvert(substring)) {
return valueParser.convert(value, index, context);
}
}
return INVALID;
Results.Errors errors = new Results.Errors();
errors.invalidValue(context.identifier.getName(), substring, context.line.get());
return errors;
}
private ValueConverters() {}
private static final ValueConverter[] PARSERS = {
MULTILINE_STRING_PARSER, MULTILINE_LITERAL_STRING_CONVERTER, LITERAL_STRING_PARSER, STRING_PARSER, DATE_PARSER, NUMBER_PARSER, BOOLEAN_PARSER, ARRAY_PARSER, INLINE_TABLE_PARSER
};
}

View file

@ -1,118 +0,0 @@
package com.moandjiezana.toml;
import java.util.ArrayList;
import java.util.List;
import org.parboiled.BaseParser;
import org.parboiled.Rule;
import org.parboiled.annotations.BuildParseTree;
import org.parboiled.parserunners.RecoveringParseRunner;
import org.parboiled.support.ParseTreeUtils;
import org.parboiled.support.ParsingResult;
@BuildParseTree
class ValueParser extends BaseParser<List<Object>> {
public static void main(String[] args) {
ParsingResult<Object> parsingResult = new RecoveringParseRunner<Object>(ValueConverterUtils.parser().T()).run("'''abc''' # comment");
System.out.println(ParseTreeUtils.printNodeTree(parsingResult));
}
public Rule T() {
return Sequence("'''", OneOrMore(TestNot("'''"), ANY), "'''", Comment());
}
public Rule Array() {
return FirstOf(EmptyArray(), Sequence('[', startList(), OneOrMore(FirstOf(NonEmptyArray(), ' ', ',')), ']', endList()));
}
public Rule Table() {
return Sequence('[', startList(), Sequence(OneOrMore(NoneOf("[]")), pushToken(match())), ']', endList(), Comment());
}
public Rule TableArray() {
return Sequence('[', '[', startList(), Sequence(OneOrMore(NoneOf("[]")), pushToken(match())), ']', ']', endList(), FirstOf(EOI, Sequence(TestNot(']'), ANY)));
}
public Rule LiteralString() {
return FirstOf(EmptyLiteralString(), Sequence('\'', OneOrMore(TestNot("'"), ANY), startList(), pushToken(match()) , '\'', endList(), Comment()));
}
public Rule MultilineLiteralString() {
return FirstOf(EmptyMultilineLiteralString(), Sequence("'''", startList(), Sequence(OneOrMore(TestNot("'''"), ANY), pushToken(match())), "'''", endList(), Comment()));
}
public Rule Boolean() {
return Sequence(startList(), FirstOf("true", "false"), pushToken(match()), endList(), Comment());
}
public Rule Integer() {
return Sequence(startList(), Sequence(SignedNumber(), pushToken(match())), endList(), Comment());
}
public Rule Exponent() {
return Sequence(startList(), Sequence(Sequence(SignedNumber(), Optional(Sequence('.', Number())), FirstOf('e', 'E'), SignedNumber()), pushToken(match())), endList(), Comment());
}
Rule NonEmptyArray() {
return FirstOf(Array(), OneOrMore(TestNot(']'), FirstOf(StringToken(), Array(), ',', ' ', OtherValue())));
}
Rule StringToken() {
return Sequence(Sequence('"', ZeroOrMore(Sequence(TestNot('"'), ANY)), '"'), pushToken(match()));
}
Rule EmptyLiteralString() {
return Sequence('\'', '\'', startList(), pushToken(""), endList());
}
Rule EmptyMultilineLiteralString() {
return Sequence("'''", "'''", startList(), pushToken(""), endList(), Comment());
}
Rule EmptyArray() {
return Sequence('[', ']', startList(), endList());
}
Rule SignedNumber() {
return Sequence(Optional(AnyOf("+-")), Number());
}
Rule Number() {
return OneOrMore(CharRange('0', '9'));
}
Rule OtherValue() {
return Sequence(ZeroOrMore(NoneOf("],")), pushToken(match()));
}
Rule Comment() {
return FirstOf(EOI, OneOrMore(' ', Sequence('#', ZeroOrMore(ANY))));
}
boolean startList() {
ArrayList<Object> newTokens = new ArrayList<Object>();
if (!getContext().getValueStack().isEmpty()) {
peek().add(newTokens);
}
push(newTokens);
return true;
}
boolean endList() {
if (getContext().getValueStack().size() > 1) {
pop();
}
return true;
}
boolean pushToken(String s) {
peek().add(s);
return true;
}
}

View file

@ -1,10 +1,14 @@
package com.moandjiezana.toml;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
@ -17,14 +21,14 @@ public class ArrayTest {
public void should_get_array() throws Exception {
Toml toml = new Toml().parse("list = [\"a\", \"b\", \"c\"]");
assertEquals(asList("a", "b", "c"), toml.getList("list", String.class));
assertEquals(asList("a", "b", "c"), toml.<String>getList("list"));
}
@Test
public void should_allow_multiline_array() throws Exception {
Toml toml = new Toml().parse(file("should_allow_multiline_array"));
assertEquals(asList("a", "b", "c"), toml.getList("a", String.class));
assertEquals(asList("a", "b", "c"), toml.<String>getList("a"));
}
@Test
@ -32,7 +36,17 @@ public class ArrayTest {
public void should_get_nested_arrays() throws Exception {
Toml clients = new Toml().parse("data = [ [\"gamma\", \"delta\"], [1, 2]] # just an update to make sure parsers support it");
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.getList("data", String.class));
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.<String>getList("data"));
}
@Test
public void should_get_deeply_nested_arrays() throws Exception {
List<List<?>> data = new Toml().parse("data = [[[1], [2]], [3, 4]]").getList("data");
assertThat(data, hasSize(2));
assertEquals(Arrays.asList(1L), data.get(0).get(0));
assertEquals(asList(2L), data.get(0).get(1));
assertEquals(asList(3L, 4L), data.get(1));
}
@Test
@ -40,7 +54,7 @@ public class ArrayTest {
public void should_get_nested_arrays_with_no_space_between_outer_and_inner_array() throws Exception {
Toml clients = new Toml().parse("data = [[\"gamma\", \"delta\"], [1, 2]] # just an update to make sure parsers support it");
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.getList("data", String.class));
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.<String>getList("data"));
}
@Test
@ -54,7 +68,40 @@ public class ArrayTest {
public void should_ignore_comma_at_end_of_array() throws Exception {
Toml toml = new Toml().parse("key=[1,2,3,]");
assertEquals(asList(1L, 2L, 3L), toml.getList("key", Long.class));
assertEquals(asList(1L, 2L, 3L), toml.<Long>getList("key"));
}
@Test
public void should_support_mixed_string_types() throws Exception {
Toml toml = new Toml().parse("key = [\"a\", 'b', \"\"\"c\"\"\", '''d''']");
assertThat(toml.<String>getList("key"), contains("a", "b", "c", "d"));
}
@Test
public void should_support_array_terminator_in_strings() throws Exception {
Toml toml = new Toml().parse("key = [\"a]\", 'b]', \"\"\"c]\"\"\", '''d]''']");
assertThat(toml.<String>getList("key"), contains("a]", "b]", "c]", "d]"));
}
@Test
public void should_support_array_of_inline_tables() throws Exception {
Toml toml = new Toml().parse(getClass().getResourceAsStream("should_support_array_of_inline_tables.toml"));
assertThat(toml.getList("points"), hasSize(4));
assertEquals(1, toml.getLong("points[0].x").longValue());
assertEquals(2, toml.getLong("points[0].y").longValue());
assertEquals(3, toml.getLong("points[0].z").longValue());
assertEquals(7, toml.getLong("points[1].x").longValue());
assertEquals(8, toml.getLong("points[1].y").longValue());
assertEquals(9, toml.getLong("points[1].z").longValue());
assertEquals(2, toml.getLong("points[2].x").longValue());
assertEquals(4, toml.getLong("points[2].y").longValue());
assertEquals(8, toml.getLong("points[2].z").longValue());
assertEquals("3", toml.getString("points[3].x"));
assertEquals("6", toml.getString("points[3].y"));
assertEquals("12", toml.getString("points[3].z"));
}
private File file(String file) {

View file

@ -0,0 +1,123 @@
package com.moandjiezana.toml;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class BareKeysTest {
@Test
public void should_ignore_spaces_around_key_segments() throws Exception {
Toml toml = new Toml().parse("[ a . b . c ] \n key = \"a\"");
assertEquals("a", toml.getString("a.b.c.key"));
}
@Test
public void should_support_underscores_in_key_names() throws Exception {
Toml toml = new Toml().parse("a_a = 1");
assertEquals(1, toml.getLong("a_a").intValue());
}
@Test
public void should_support_underscores_in_table_names() throws Exception {
Toml toml = new Toml().parse("[group_a]\na = 1");
assertEquals(1, toml.getLong("group_a.a").intValue());
}
@Test
public void should_support_numbers_in_key_names() throws Exception {
Toml toml = new Toml().parse("a1 = 1");
assertEquals(1, toml.getLong("a1").intValue());
}
@Test
public void should_support_numbers_in_table_names() throws Exception {
Toml toml = new Toml().parse("[group1]\na = 1");
assertEquals(1, toml.getLong("group1.a").intValue());
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_characters_outside_accept_range_are_used_in_table_name() throws Exception {
new Toml().parse("[~]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_characters_outside_accept_range_are_used_in_table_array_name() throws Exception {
new Toml().parse("[[~]]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_dots_in_key_name() throws Exception {
new Toml().parse("a.b = 1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_characters_outside_accept_range_are_used_in_key_name() throws Exception {
new Toml().parse("~ = 1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_sharp_sign_in_table_name() throws Exception {
new Toml().parse("[group#]\nkey=1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_spaces_in_table_name() throws Exception {
new Toml().parse("[valid key]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_sharp_sign_in_table_array_name() throws Exception {
new Toml().parse("[[group#]]\nkey=1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_spaces_in_table_array_name() throws Exception {
new Toml().parse("[[valid key]]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_question_marks_in_key_name() throws Exception {
new Toml().parse("key?=true");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_empty_table_name() {
new Toml().parse("[]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_nested_table_name_ending_with_empty_table_name() {
new Toml().parse("[a.]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_nested_table_name_containing_empty_table_name() {
new Toml().parse("[a..b]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_nested_table_name_starting_with_empty_table_name() {
new Toml().parse("[.b]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_nested_table_array_name_ending_with_empty_table_name() {
new Toml().parse("[[a.]]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_nested_table_array_name_containing_empty_table_name() {
new Toml().parse("[[a..b]]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_nested_table_array_name_starting_with_empty_table_name() {
new Toml().parse("[[.b]]");
}
}

View file

@ -14,6 +14,7 @@ import java.util.TimeZone;
import org.junit.After;
import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;
import com.google.gson.Gson;
@ -108,21 +109,36 @@ public class BurntSushiValidTest {
run("key-equals-nospace");
}
@Test
@Test @Ignore
public void key_space() throws Exception {
run("key-space");
}
@Test
public void key_space_modified() throws Exception {
run("key-space-modified");
}
@Test @Ignore
public void key_special_chars() throws Exception {
run("key-special-chars");
}
@Test
public void key_special_chars_modified() throws Exception {
run("key-special-chars-modified");
}
@Test @Ignore
public void keys_with_dots() throws Exception {
run("keys-with-dots");
}
@Test
public void keys_with_dots_modified() throws Exception {
run("keys-with-dots-modified");
}
@Test
public void long_float() throws Exception {
run("long-float");
@ -133,11 +149,16 @@ public class BurntSushiValidTest {
run("long-integer");
}
@Test
@Test @Ignore
public void multiline_string() throws Exception {
run("multiline-string");
}
@Test
public void multiline_string_modified() throws Exception {
run("multiline-string-modified");
}
@Test
public void raw_multiline_string() throws Exception {
run("raw-multiline-string");
@ -153,9 +174,14 @@ public class BurntSushiValidTest {
run("string-empty");
}
@Test
@Test @Ignore
public void string_escapes() throws Exception {
run("string-escapes");
run("string-escapes-modified");
}
@Test
public void string_escapes_modified() throws Exception {
run("string-escapes-modified");
}
@Test
@ -198,12 +224,12 @@ public class BurntSushiValidTest {
run("table-sub-empty");
}
@Test
@Test @Ignore
public void table_whitespace() throws Exception {
run("table-whitespace");
}
@Test
@Test @Ignore
public void table_with_pound() throws Exception {
run("table-with-pound");
}

View file

@ -0,0 +1,123 @@
package com.moandjiezana.toml;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class ErrorMessagesTest {
@Rule
public final ExpectedException e = ExpectedException.none();
@Test
public void should_message_invalid_table() throws Exception {
e.expectMessage("Invalid table definition on line 1: [in valid]");
new Toml().parse("[in valid]");
}
@Test
public void should_message_duplicate_table() throws Exception {
e.expectMessage("Duplicate table definition on line 2: [again]");
new Toml().parse("[again]\n[again]");
}
@Test
public void should_message_empty_implicit_table_name() throws Exception {
e.expectMessage("Invalid table definition due to empty implicit table name: [a..b]");
new Toml().parse("[a..b]");
}
@Test
public void should_message_duplicate_key() throws Exception {
e.expectMessage("Duplicate key on line 2: k");
new Toml().parse("k = 1\n k = 2");
}
@Test
public void should_message_invalid_key() throws Exception {
e.expectMessage("Key is not followed by an equals sign on line 1: k\" = 1");
new Toml().parse("k\" = 1");
}
@Test
public void should_message_invalid_table_array() throws Exception {
e.expectMessage("Invalid table array definition on line 1: [[in valid]]");
new Toml().parse("[[in valid]]");
}
@Test
public void should_message_invalid_value() throws Exception {
e.expectMessage("Invalid text after key k on line 1");
new Toml().parse("k = 1 t");
}
@Test
public void should_message_unterminated_multiline_literal_string() throws Exception {
e.expectMessage("Unterminated value on line 1: k = '''abc");
new Toml().parse("k = '''abc");
}
@Test
public void should_message_unterminated_multiline_string() throws Exception {
e.expectMessage("Unterminated value on line 1: k = \"\"\"abc\"\"");
new Toml().parse("k = \"\"\"abc\"\"");
}
@Test
public void should_message_unterminated_array() throws Exception {
e.expectMessage("Unterminated value on line 1: k = [\"abc\"");
new Toml().parse("k = [\"abc\"");
}
@Test
public void should_message_unterminated_inline_table() throws Exception {
e.expectMessage("Unterminated value on line 1: k = { a = \"abc\"");
new Toml().parse("k = { a = \"abc\"");
}
@Test
public void should_message_key_without_equals() throws Exception {
e.expectMessage("Key is not followed by an equals sign on line 2: k");
new Toml().parse("\nk\n=3");
}
@Test
public void should_display_correct_line_number_with_literal_multiline_string() throws Exception {
e.expectMessage("on line 8");
new Toml().parse("[table]\n\n k = '''abc\n\ndef\n'''\n # comment \n j = 4.\n l = 5");
}
@Test
public void should_display_correct_line_number_with_multiline_string() throws Exception {
e.expectMessage("on line 9");
new Toml().parse("[table]\n\n k = \"\"\"\nabc\n\ndef\n\"\"\"\n # comment \n j = 4.\n l = 5");
}
@Test
public void should_display_correct_line_number_with_array() throws Exception {
e.expectMessage("on line 10");
new Toml().parse("[table]\n\n k = [\"\"\"\nabc\n\ndef\n\"\"\"\n, \n # comment \n j = 4.,\n l = 5\n]");
}
@Test
public void should_message_heterogeneous_array() throws Exception {
e.expectMessage("k becomes a heterogeneous array on line 2");
new Toml().parse("k = [ 1,\n 1.1 ]");
}
}

View file

@ -0,0 +1,237 @@
package com.moandjiezana.toml;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
public class InlineTableTest {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
@Rule
public ExpectedException e = ExpectedException.none();
@Test
public void should_read_empty_inline_table() throws Exception {
Toml toml = new Toml().parse("key = {}");
assertNotNull(toml.getTable("key"));
}
@Test
public void should_read_inline_table_with_strings() throws Exception {
Toml toml = new Toml().parse("name = { first = \"Tom\", last = \"Preston-Werner\"}");
assertEquals("Tom", toml.getTable("name").getString("first"));
assertEquals("Preston-Werner", toml.getString("name.last"));
}
@Test
public void should_read_inline_table_with_integers() throws Exception {
Toml toml = new Toml().parse("point = { x = 1, y = 2 }");
assertEquals(1, toml.getTable("point").getLong("x").longValue());
assertEquals(2, toml.getLong("point.y").longValue());
}
@Test
public void should_read_inline_table_with_floats() throws Exception {
Toml toml = new Toml().parse("point = { x = 1.5, y = 2.3 }");
assertEquals(1.5, toml.getTable("point").getDouble("x").doubleValue(), 0);
assertEquals(2.3, toml.getDouble("point.y").doubleValue(), 0);
}
@Test
public void should_read_inline_table_with_booleans() throws Exception {
Toml toml = new Toml().parse("point = { x = false, y = true }");
assertTrue(toml.getTable("point").getBoolean("y"));
assertFalse(toml.getBoolean("point.x"));
}
@Test
public void should_read_inline_table_with_dates() throws Exception {
Toml toml = new Toml().parse("point = { x = 2015-02-09T22:05:00Z, y = 2015-02-09T21:05:00Z }");
Calendar x = Calendar.getInstance(UTC);
x.set(2015, Calendar.FEBRUARY, 9, 22, 5, 00);
x.set(Calendar.MILLISECOND, 0);
Calendar y = Calendar.getInstance(UTC);
y.set(2015, Calendar.FEBRUARY, 9, 21, 5, 00);
y.set(Calendar.MILLISECOND, 0);
assertEquals(x.getTime(), toml.getTable("point").getDate("x"));
assertEquals(y.getTime(), toml.getDate("point.y"));
}
@Test
public void should_read_arrays() throws Exception {
Toml toml = new Toml().parse("arrays = { integers = [1, 2, 3], strings = [\"a\", \"b\", \"c\"] }");
assertThat(toml.<Long>getList("arrays.integers"), contains(1L, 2L, 3L));
assertThat(toml.<String>getList("arrays.strings"), contains("a", "b", "c"));
}
@Test
public void should_read_nested_arrays() throws Exception {
Toml toml = new Toml().parse("arrays = { nested = [[1, 2, 3], [4, 5, 6]] }").getTable("arrays");
List<List<Long>> nested = toml.<List<Long>>getList("nested");
assertThat(nested, hasSize(2));
assertThat(nested.get(0), contains(1L, 2L, 3L));
assertThat(nested.get(1), contains(4L, 5L, 6L));
}
@Test
public void should_read_mixed_inline_table() throws Exception {
Toml toml = new Toml().parse("point = { date = 2015-02-09T22:05:00Z, bool = true, integer = 123, float = 123.456, string = \"abc\", list = [5, 6, 7, 8] }").getTable("point");
Calendar date = Calendar.getInstance(UTC);
date.set(2015, Calendar.FEBRUARY, 9, 22, 5, 00);
date.set(Calendar.MILLISECOND, 0);
assertEquals(date.getTime(), toml.getDate("date"));
assertTrue(toml.getBoolean("bool"));
assertEquals(123, toml.getLong("integer").intValue());
assertEquals(123.456, toml.getDouble("float"), 0);
assertEquals("abc", toml.getString("string"));
assertThat(toml.<Long>getList("list"), contains(5L, 6L, 7L, 8L));
}
@Test
public void should_read_nested_inline_tables() throws Exception {
Toml tables = new Toml().parse("tables = { t1 = { t1_1 = 1, t1_2 = 2}, t2 = { t2_1 = { t2_1_1 = \"a\" }} }").getTable("tables");
assertEquals(1L, tables.getLong("t1.t1_1").longValue());
assertEquals(2L, tables.getLong("t1.t1_2").longValue());
assertEquals("a", tables.getString("t2.t2_1.t2_1_1"));
}
@Test
public void should_read_all_string_types() throws Exception {
Toml strings = new Toml().parse("strings = { literal = 'ab]\"c', multiline = \"\"\"de]\"f\"\"\", multiline_literal = '''gh]\"i''' }").getTable("strings");
assertEquals("ab]\"c", strings.getString("literal"));
assertEquals("de]\"f", strings.getString("multiline"));
assertEquals("gh]\"i", strings.getString("multiline_literal"));
}
@Test
public void should_read_inline_table_in_regular_table() throws Exception {
Toml toml = new Toml().parse("[tbl]\n tbl = { tbl = 1 }");
assertEquals(1, toml.getLong("tbl.tbl.tbl").intValue());
}
@Test
public void should_mix_with_tables() throws Exception {
Toml toml = new Toml().parse("t = { k = 1 }\n [b]\n k = 2\n t = { k = 3}");
assertEquals(1, toml.getLong("t.k").intValue());
assertEquals(2, toml.getLong("b.k").intValue());
assertEquals(3, toml.getLong("b.t.k").intValue());
}
@Test
public void should_add_properties_to_existing_inline_table() throws Exception {
Toml toml = new Toml().parse("[a]\n b = {k = 1}\n [a.b.c]\n k = 2");
assertEquals(1, toml.getLong("a.b.k").intValue());
assertEquals(2, toml.getLong("a.b.c.k").intValue());
}
@Test
public void should_mix_with_table_arrays() throws Exception {
Toml toml = new Toml().parse("t = { k = 1 }\n [[b]]\n t = { k = 2 }\n [[b]]\n t = { k = 3 }");
assertEquals(1, toml.getLong("t.k").intValue());
assertEquals(2, toml.getLong("b[0].t.k").intValue());
assertEquals(3, toml.getLong("b[1].t.k").intValue());
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_invalid_key() throws Exception {
new Toml().parse("tbl = { a. = 1 }");
}
@Test(expected = IllegalStateException.class)
public void should_fail_when_unterminated() throws Exception {
new Toml().parse("tbl = { a = 1 ");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_invalid_value() throws Exception {
new Toml().parse("tbl = { a = abc }");
}
@Test
public void should_fail_when_key_duplicated_inside_inline_table() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate key on line 1: a");
new Toml().parse("tbl = { a = 1, a = 2 }");
}
@Test
public void should_fail_when_duplicated_by_other_key() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate key on line 2: tbl");
new Toml().parse("tbl = { a = 1 }\n tbl = 1");
}
@Test
public void should_fail_when_duplicated_by_other_inline_table() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate table definition on line 2: [tbl]");
new Toml().parse("tbl = { a = 1 }\n tbl = {}");
}
@Test
public void should_fail_when_duplicated_by_top_level_table() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate table definition on line 2: [tbl]");
new Toml().parse("tbl = {}\n [tbl]");
}
@Test
public void should_fail_when_duplicates_second_level_table() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate table definition on line 3: [a.b]");
new Toml().parse("[a.b]\n [a]\n b = {}");
}
@Test
public void should_fail_when_inline_table_duplicates_table() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate table definition on line 3: [a.b]");
new Toml().parse("[a.b]\n [a]\n b = {}");
}
@Test
public void should_fail_when_second_level_table_duplicates_inline_table() throws Exception {
e.expect(IllegalStateException.class);
e.expectMessage("Duplicate table definition on line 3: [a.b]");
new Toml().parse("[a]\n b = {}\n [a.b]");
}
}

View file

@ -51,14 +51,14 @@ public class NumberTest {
public void should_get_exponent() throws Exception {
Toml toml = new Toml().parse("lower_case = 1e6\nupper_case = 2E6\nwith_plus = 5e+22\nboth_plus = +5E+22\nnegative = -2E-2\nfractional = 6.626e-34");
assertEquals(Math.pow(1, 6), toml.getDouble("lower_case"), 0.0);
assertEquals(Math.pow(2, 6), toml.getDouble("upper_case"), 0.0);
assertEquals(Math.pow(5, 22), toml.getDouble("with_plus"), 0.0);
assertEquals(Math.pow(5, 22), toml.getDouble("both_plus"), 0.0);
assertEquals(Math.pow(-2, -2), toml.getDouble("negative"), 0.0);
assertEquals(Math.pow(6.626D, -34), toml.getDouble("fractional"), 0.0);
assertEquals(1e6, toml.getDouble("lower_case"), 0.0);
assertEquals(2E6, toml.getDouble("upper_case"), 0.0);
assertEquals(5e22, toml.getDouble("with_plus"), 0.0);
assertEquals(5e22, toml.getDouble("both_plus"), 0.0);
assertEquals(-2e-2, toml.getDouble("negative"), 0.0);
assertEquals(6.626D * Math.pow(10, -34), toml.getDouble("fractional"), 0.0);
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_invalid_number() throws Exception {
new Toml().parse("a = 200-");
@ -84,6 +84,12 @@ public class NumberTest {
new Toml().parse("answer = -.12345");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_float_with_sign_after_dot() {
new Toml().parse("answer = 1.-1");
new Toml().parse("answer = 1.+1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_float_without_digits_after_dot() {
new Toml().parse("answer = 1.");
@ -93,4 +99,34 @@ public class NumberTest {
public void should_fail_on_negative_float_without_digits_after_dot() {
new Toml().parse("answer = -1.");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_exponent_without_digits_after_dot() {
new Toml().parse("answer = 1.E1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_negative_exponent_without_digits_after_dot() {
new Toml().parse("answer = -1.E1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_exponent_with_dot_in_exponent_part() {
new Toml().parse("answer = -1E1.0");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_exponent_without_numbers_after_E() {
new Toml().parse("answer = -1E");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_exponent_with_two_E() {
new Toml().parse("answer = -1E1E1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_float_with_two_dots() {
new Toml().parse("answer = 1.1.1");
}
}

View file

@ -0,0 +1,137 @@
package com.moandjiezana.toml;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import java.util.Map;
import org.junit.Test;
public class QuotedKeysTest {
@Test
public void should_accept_quoted_key_for_value() throws Exception {
Toml toml = new Toml().parse("\"127.0.0.1\" = \"localhost\" \n \"character encoding\" = \"UTF-8\" \n \"ʎǝʞ\" = \"value\"");
assertEquals("localhost", toml.getString("\"127.0.0.1\""));
assertEquals("UTF-8", toml.getString("\"character encoding\""));
assertEquals("value", toml.getString("\"ʎǝʞ\""));
}
@Test
public void should_accept_quoted_key_for_table_name() throws Exception {
Toml toml = new Toml().parse("[\"abc def\"]\n val = 1");
assertEquals(1L, toml.getTable("\"abc def\"").getLong("val").longValue());
}
@Test
public void should_accept_partially_quoted_table_name() throws Exception {
Toml toml = new Toml().parse("[dog.\"tater.man\"] \n type = \"pug0\" \n[dog.tater] \n type = \"pug1\"\n[dog.tater.man] \n type = \"pug2\"");
Toml dogs = toml.getTable("dog");
assertEquals("pug0", dogs.getTable("\"tater.man\"").getString("type"));
assertEquals("pug1", dogs.getTable("tater").getString("type"));
assertEquals("pug2", dogs.getTable("tater").getTable("man").getString("type"));
assertEquals("pug0", toml.getString("dog.\"tater.man\".type"));
assertEquals("pug2", toml.getString("dog.tater.man.type"));
}
@Test
@SuppressWarnings("unchecked")
public void should_conserve_quoted_key_in_map() throws Exception {
Toml toml = new Toml().parse("[dog.\"tater.man\"] \n type = \"pug0\" \n[dog.tater] \n type = \"pug1\"\n[dog.tater.man] \n type = \"pug2\"");
Toml dogs = toml.getTable("dog");
Map<String, Map<String, Object>> map = dogs.to(Map.class);
assertEquals("pug0", map.get("\"tater.man\"").get("type"));
assertEquals("pug1", map.get("tater").get("type"));
assertEquals("pug2", ((Map<String, Object>) map.get("tater").get("man")).get("type"));
}
@Test
public void should_convert_quoted_keys_to_map_but_not_to_object_fields() throws Exception {
Quoted quoted = new Toml().parse("\"ʎǝʞ\" = \"value\" \n[map] \n \"ʎǝʞ\" = \"value\"").to(Quoted.class);
assertNull(quoted.ʎǝʞ);
assertEquals("value", quoted.map.get("\"ʎǝʞ\""));
}
@Test
public void should_support_table_array_index_with_quoted_key() throws Exception {
Toml toml = new Toml().parse("[[ dog. \" type\" ]] \n name = \"type0\" \n [[dog.\" type\"]] \n name = \"type1\"");
assertEquals("type0", toml.getString("dog.\" type\"[0].name"));
assertEquals("type1", toml.getString("dog.\" type\"[1].name"));
}
@Test
public void should_support_table_array_index_with_dot_in_quoted_key() throws Exception {
Toml toml = new Toml().parse("[[ dog. \"a.type\" ]] \n name = \"type0\"");
assertEquals("type0", toml.getString("dog.\"a.type\"[0].name"));
}
@Test
public void should_support_quoted_key_containing_square_brackets() throws Exception {
Toml toml = new Toml().parse("[dog.\" type[abc]\"] \n name = \"type0\" \n [dog.\" type[1]\"] \n \"name[]\" = \"type1\"");
assertEquals("type0", toml.getString("dog.\" type[abc]\".name"));
assertEquals("type1", toml.getString("dog.\" type[1]\".\"name[]\""));
}
@Test
public void should_support_quoted_key_containing_escaped_quote() throws Exception {
Toml toml = new Toml().parse("[dog.\"ty\\\"pe\"] \n \"na\\\"me\" = \"type0\"");
assertEquals("type0", toml.getString("dog.\"ty\\\"pe\".\"na\\\"me\""));
}
@Test
public void should_support_fully_quoted_table_name() throws Exception {
Toml toml = new Toml().parse("[\"abc.def\"] \n key = 1");
assertEquals(1, toml.getLong("\"abc.def\".key").intValue());
}
@Test
public void should_support_whitespace_around_key_segments() throws Exception {
Toml toml = new Toml().parse("[ dog. \"type\". breed ] \n name = \"type0\"");
assertEquals("type0", toml.getString("dog.\"type\".breed.name"));
}
@Test
public void should_support_unicode() throws Exception {
Toml toml = new Toml().parse("[[\"\\u00B1\"]]\n \"\\u00B1\" = \"a\"\n [\"\\u00B11\"]\n \"±\" = 1");
assertThat(toml.getTables("\"±\""), hasSize(1));
assertEquals("a", toml.getTables("\"±\"").get(0).getString("\"±\""));
assertEquals(1, toml.getTable("\"±1\"").getLong("\"±\"").intValue());
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_malformed_quoted_key() throws Exception {
new Toml().parse("k\"ey\" = 1");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_malformed_quoted_table() throws Exception {
new Toml().parse("[a\"bc\"]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_malformed_quoted_nested_table() throws Exception {
new Toml().parse("[a.a\"bc\"]");
}
private static class Quoted {
String ʎǝʞ;
Map<String, Object> map;
}
}

View file

@ -1,13 +1,17 @@
package com.moandjiezana.toml;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.contains;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.TimeZone;
import org.junit.Test;
@ -38,7 +42,7 @@ public class RealWorldTest {
assertEquals("192.168.1.1", database.getString("server"));
assertEquals(5000L, database.getLong("connection_max").longValue());
assertTrue(database.getBoolean("enabled"));
assertEquals(Arrays.asList(8001L, 8001L, 8002L), database.getList("ports", Long.class));
assertEquals(Arrays.asList(8001L, 8001L, 8002L), database.<Long>getList("ports"));
Toml servers = toml.getTable("servers");
Toml alphaServers = servers.getTable("alpha");
@ -50,8 +54,8 @@ public class RealWorldTest {
assertEquals("中国", betaServers.getString("country"));
Toml clients = toml.getTable("clients");
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.getList("data", String.class));
assertEquals(asList("alpha", "omega"), clients.getList("hosts", String.class));
assertEquals(asList(asList("gamma", "delta"), asList(1L, 2L)), clients.<String>getList("data"));
assertEquals(asList("alpha", "omega"), clients.<String>getList("hosts"));
}
@Test
@ -59,13 +63,77 @@ public class RealWorldTest {
Toml toml = new Toml().parse(new File(getClass().getResource("hard_example.toml").getFile()));
assertEquals("You'll hate me after this - #", toml.getString("the.test_string"));
assertEquals(asList("] ", " # "), toml.getList("the.hard.test_array", String.class));
assertEquals(asList("Test #11 ]proved that", "Experiment #9 was a success"), toml.getList("the.hard.test_array2", String.class));
assertEquals(asList("] ", " # "), toml.<String>getList("the.hard.test_array"));
assertEquals(asList("Test #11 ]proved that", "Experiment #9 was a success"), toml.<String>getList("the.hard.test_array2"));
assertEquals(" Same thing, but with a string #", toml.getString("the.hard.another_test_string"));
assertEquals(" And when \"'s are in the string, along with # \"", toml.getString("the.hard.harder_test_string"));
Toml theHardBit = toml.getTable("the.hard.bit#");
assertEquals("You don't think some user won't do that?", theHardBit.getString("what?"));
assertEquals(asList("]"), theHardBit.getList("multi_line_array", String.class));
Toml theHardBit = toml.getTable("the.hard.\"bit#\"");
assertEquals("You don't think some user won't do that?", theHardBit.getString("\"what?\""));
assertEquals(asList("]"), theHardBit.<String>getList("multi_line_array"));
}
@SuppressWarnings("unchecked")
@Test
public void should_parse_current_version_example() throws Exception {
Toml toml = new Toml().parse(new File(getClass().getResource("example-v0.3.0.toml").getFile()));
assertEquals("value", toml.getString("Table.key"));
assertEquals("pug", toml.getString("dog.tater.type"));
assertNotNull(toml.getTable("x.y.z").getTable("w"));
assertEquals("I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF.", toml.getString("String.basic"));
assertEquals("One\nTwo", toml.getString("String.Multiline.key3"));
assertEquals(toml.getString("String.Multiline.key3"), toml.getString("String.Multiline.key1"));
assertEquals(toml.getString("String.Multiline.key3"), toml.getString("String.Multiline.key2"));
assertEquals("The quick brown fox jumps over the lazy dog.", toml.getString("String.Multilined.Singleline.key3"));
assertEquals(toml.getString("String.Multilined.Singleline.key3"), toml.getString("String.Multilined.Singleline.key1"));
assertEquals(toml.getString("String.Multilined.Singleline.key3"), toml.getString("String.Multilined.Singleline.key2"));
assertEquals("C:\\Users\\nodejs\\templates", toml.getString("String.Literal.winpath"));
assertEquals("\\\\ServerX\\admin$\\system32\\", toml.getString("String.Literal.winpath2"));
assertEquals("Tom \"Dubs\" Preston-Werner", toml.getString("String.Literal.quoted"));
assertEquals("<\\i\\c*\\s*>", toml.getString("String.Literal.regex"));
assertEquals("I [dw]on't need \\d{2} apples", toml.getString("String.Literal.Multiline.regex2"));
assertEquals("The first newline is\ntrimmed in raw strings.\n All other whitespace\n is preserved.\n", toml.getString("String.Literal.Multiline.lines"));
assertEquals(99, toml.getLong("Integer.key1").intValue());
assertEquals(42, toml.getLong("Integer.key2").intValue());
assertEquals(0, toml.getLong("Integer.key3").intValue());
assertEquals(-17, toml.getLong("Integer.key4").intValue());
assertEquals(1.0, toml.getDouble("Float.fractional.key1").doubleValue(), 0);
assertEquals(3.1415, toml.getDouble("Float.fractional.key2").doubleValue(), 0);
assertEquals(-0.01, toml.getDouble("Float.fractional.key3").doubleValue(), 0);
assertEquals(5e+22, toml.getDouble("Float.exponent.key1").doubleValue(), 0);
assertEquals(1e6, toml.getDouble("Float.exponent.key2").longValue(), 0);
assertEquals(-2E-2, toml.getDouble("Float.exponent.key3").doubleValue(), 0);
assertEquals(6.626e-34, toml.getDouble("Float.both.key").doubleValue(), 0);
assertTrue(toml.getBoolean("Booleans.True"));
assertFalse(toml.getBoolean("Booleans.False"));
assertThat(toml.<Long>getList("Array.key1"), contains(1L, 2L, 3L));
assertThat(toml.<String>getList("Array.key2"), contains("red", "yellow", "green"));
assertEquals(asList(asList(1L, 2L), asList(3L, 4L, 5L)), toml.<List<Long>>getList("Array.key3"));
assertEquals(asList(asList(1L, 2L), asList("a", "b", "c")), toml.<List<Long>>getList("Array.key4"));
assertThat(toml.<Long>getList("Array.key5"), contains(1L, 2L, 3L));
assertThat(toml.<Long>getList("Array.key6"), contains(1L, 2L));
assertEquals("Hammer", toml.getString("products[0].name"));
assertEquals(738594937, toml.getLong("products[0].sku").intValue());
assertNotNull(toml.getTable("products[1]"));
assertEquals("Nail", toml.getString("products[2].name"));
assertEquals(284758393, toml.getLong("products[2].sku").intValue());
assertEquals("gray", toml.getString("products[2].color"));
assertEquals("apple", toml.getString("fruit[0].name"));
assertEquals("red", toml.getString("fruit[0].physical.color"));
assertEquals("round", toml.getString("fruit[0].physical.shape"));
assertEquals("red delicious", toml.getString("fruit[0].variety[0].name"));
assertEquals("granny smith", toml.getString("fruit[0].variety[1].name"));
assertEquals("banana", toml.getString("fruit[1].name"));
assertEquals("plantain", toml.getString("fruit[1].variety[0].name"));
Calendar dob = Calendar.getInstance();
dob.set(1979, Calendar.MAY, 27, 7, 32, 0);
dob.set(Calendar.MILLISECOND, 0);
dob.setTimeZone(TimeZone.getTimeZone("UTC"));
assertEquals(dob.getTime(), toml.getDate("Datetime.key1"));
assertEquals(dob.getTime(), toml.getDate("Datetime.key2"));
dob.set(Calendar.MILLISECOND, 999);
assertEquals(dob.getTime(), toml.getDate("Datetime.key3"));
}
@Test

View file

@ -69,7 +69,7 @@ public class StringTest {
public void should_support_special_characters_in_strings() {
Toml toml = new Toml().parse(new File(getClass().getResource("should_support_special_characters_in_strings.toml").getFile()));
assertEquals("\" \t \n \r \\ / \b \f", toml.getString("key"));
assertEquals("\" \t \n \r \\ \b \f", toml.getString("key"));
}
@Test
@ -85,6 +85,11 @@ public class StringTest {
new Toml().parse("key=\"\\m\"");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_escaped_slash() throws Exception {
new Toml().parse("key=\"\\/\"");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_text_after_literal_string() {
new Toml().parse("a = ' ' jdkf");
@ -96,10 +101,15 @@ public class StringTest {
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_unterminated_multiline_literal_string() throws Exception {
public void should_fail_on_multiline_literal_string_with_malformed_comment() throws Exception {
new Toml().parse("a = '''some\n text\n''\nb = '''1'''");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_unterminated_multiline_literal_string() throws Exception {
new Toml().parse("a = '''some\n text\n''");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_unterminated_multiline_literal_string_on_single_line() throws Exception {
new Toml().parse("a = '''some text''");

View file

@ -87,6 +87,11 @@ public class TableArrayTest {
assertEquals("granny smith", appleVariety.getString("name"));
assertEquals("plantain", bananaVariety);
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_empty_table_array_name() {
new Toml().parse("[[]]");
}
private File file(String fileName) {
return new File(getClass().getResource(fileName + ".toml").getFile());

View file

@ -1,17 +1,16 @@
package com.moandjiezana.toml;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.util.Calendar;
import java.util.Map;
import java.util.TimeZone;
import org.fest.reflect.core.Reflection;
import org.hamcrest.Matchers;
import org.junit.Test;
@ -60,6 +59,17 @@ public class TomlTest {
assertEquals(2, toml.getLong("a.d").intValue());
assertEquals(1, toml.getTable("a.b").getLong("c").intValue());
}
@Test
public void should_handle_navigation_to_missing_value() throws Exception {
Toml toml = new Toml();
assertNull(toml.getString("a.b"));
assertNull(toml.getString("a.b[0].c"));
assertThat(toml.getList("a.b"), hasSize(0));
assertTrue(toml.getTable("a.b").isEmpty());
assertTrue(toml.getTable("a.b[0]").isEmpty());
}
@Test
public void should_return_null_if_no_value_for_key() throws Exception {
@ -72,7 +82,7 @@ public class TomlTest {
public void should_return_empty_list_if_no_value_for_key() throws Exception {
Toml toml = new Toml().parse("");
assertTrue(toml.getList("a", String.class).isEmpty());
assertTrue(toml.<String>getList("a").isEmpty());
}
@Test
@ -82,14 +92,6 @@ public class TomlTest {
assertNull(toml.getString("group.key"));
}
@Test
public void should_return_empty_toml_when_no_value_for_table() throws Exception {
Toml toml = new Toml().parse("[a]").getTable("b");
assertTrue(Reflection.field("values").ofType(Map.class).in(toml).get().isEmpty());
assertNull(toml.getString("x"));
}
@Test
public void should_load_from_file() throws Exception {
Toml toml = new Toml().parse(new File(getClass().getResource("should_load_from_file.toml").getFile()));
@ -97,70 +99,6 @@ public class TomlTest {
assertEquals("value", toml.getString("key"));
}
@Test
public void should_support_numbers_in_key_names() throws Exception {
Toml toml = new Toml().parse("a1 = 1");
assertEquals(1, toml.getLong("a1").intValue());
}
@Test
public void should_support_numbers_in_table_names() throws Exception {
Toml toml = new Toml().parse("[group1]\na = 1");
assertEquals(1, toml.getLong("group1.a").intValue());
}
@Test
public void should_support_underscores_in_key_names() throws Exception {
Toml toml = new Toml().parse("a_a = 1");
assertEquals(1, toml.getLong("a_a").intValue());
}
@Test
public void should_support_question_marks_in_key_names() throws Exception {
Toml toml = new Toml().parse("key?=true");
assertTrue(toml.getBoolean("key?"));
}
@Test
public void should_support_dots_in_key_names() throws Exception {
Toml toml = new Toml().parse(file("should_support_dots_in_key_names"));
assertEquals(1, toml.getLong("a").intValue());
assertEquals(2, toml.getLong("b.c").intValue());
assertEquals(3, toml.getTable("b").getLong("c").intValue());
assertEquals(4, toml.getLong("b.a.b").intValue());
assertEquals(5, toml.getLong("d.e.a").intValue());
assertEquals(6, toml.getLong("d.e.a.b.c").intValue());
assertEquals(6, toml.getTable("d.e").getLong("a.b.c").intValue());
assertEquals(7, toml.getTables("f").get(0).getLong("a.b").intValue());
assertEquals(8, toml.getLong("f[1].a.b").intValue());
}
@Test
public void should_support_underscores_in_table_names() throws Exception {
Toml toml = new Toml().parse("[group_a]\na = 1");
assertEquals(1, toml.getLong("group_a.a").intValue());
}
@Test
public void should_support_sharp_sign_in_table_names() throws Exception {
Toml toml = new Toml().parse("[group#]\nkey=1");
assertEquals(1, toml.getLong("group#.key").intValue());
}
@Test
public void should_support_spaces_in_table_names() throws Exception {
Toml toml = new Toml().parse("[valid key]");
assertNotNull(toml.getTable("valid key"));
}
@Test
public void should_support_blank_lines() throws Exception {
Toml toml = new Toml().parse(new File(getClass().getResource("should_support_blank_line.toml").getFile()));
@ -180,7 +118,7 @@ public class TomlTest {
cal.set(Calendar.MILLISECOND, 0);
cal.setTimeZone(TimeZone.getTimeZone("UTC"));
assertEquals(cal.getTime(), toml.getDate("d"));
assertThat(toml.getList("e", String.class), Matchers.contains("a", "b"));
assertThat(toml.<String>getList("e"), Matchers.contains("a", "b"));
assertTrue(toml.getBoolean("f"));
assertEquals("abc", toml.getString("g"));
assertEquals("abc", toml.getString("h"));
@ -188,6 +126,15 @@ public class TomlTest {
assertEquals("abc\nabc", toml.getString("j"));
}
@Test
public void should_be_empty_if_no_values() throws Exception {
assertTrue(new Toml().isEmpty());
Toml toml = new Toml().parse("[a]");
assertTrue(toml.getTable("a").isEmpty());
assertTrue(toml.getTable("b").isEmpty());
assertFalse(toml.isEmpty());
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_empty_key_name() throws Exception {
new Toml().parse(" = 1");
@ -222,28 +169,4 @@ public class TomlTest {
public void should_fail_when_illegal_characters_after_table() throws Exception {
new Toml().parse("[error] if you didn't catch this, your parser is broken");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_empty_table_name() {
new Toml().parse("[]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_compound_table_name_ending_with_empty_table_name() {
new Toml().parse("[a.]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_compound_table_name_containing_empty_table_name() {
new Toml().parse("[a..b]");
}
@Test(expected = IllegalStateException.class)
public void should_fail_on_compound_table_name_starting_with_empty_table_name() {
new Toml().parse("[.b]");
}
private File file(String file) {
return new File(getClass().getResource(file + ".toml").getFile());
}
}

View file

@ -0,0 +1,22 @@
package com.moandjiezana.toml;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class UnicodeTest {
@Test
public void should_support_short_escape_form() throws Exception {
Toml toml = new Toml().parse("key = \"Jos\u00E9\\nLocation\tSF\"");
assertEquals("José\nLocation\tSF", toml.getString("key"));
}
@Test
public void should_support_unicode_literal() throws Exception {
Toml toml = new Toml().parse("key = \"José LöcÄtion SF\"");
assertEquals("José LöcÄtion SF", toml.getString("key"));
}
}

View file

@ -0,0 +1,3 @@
{
"ab": {"type": "integer", "value": "1"}
}

View file

@ -0,0 +1,5 @@
{
"_-1234567890": {
"type": "integer", "value": "1"
}
}

View file

@ -0,0 +1,14 @@
{
"plain": {"type": "integer", "value": "1"},
"\"with.dot\"": {"type": "integer", "value": "2"},
"plain_table": {
"plain": {"type": "integer", "value": "3"},
"\"with.dot\"": {"type": "integer", "value": "4"}
},
"table": {
"withdot": {
"plain": {"type": "integer", "value": "5"},
"\"key.with.dots\"": {"type": "integer", "value": "6"}
}
}
}

View file

@ -0,0 +1,10 @@
plain = 1
"with.dot" = 2
[plain_table]
plain = 3
"with.dot" = 4
[table.withdot]
plain = 5
"key.with.dots" = 6

View file

@ -0,0 +1,30 @@
{
"multiline_empty_one": {
"type": "string",
"value": ""
},
"multiline_empty_two": {
"type": "string",
"value": ""
},
"multiline_empty_three": {
"type": "string",
"value": ""
},
"multiline_empty_four": {
"type": "string",
"value": ""
},
"equivalent_one": {
"type": "string",
"value": "The quick brown fox jumps over the lazy dog."
},
"equivalent_two": {
"type": "string",
"value": "The quick brown fox jumps over the lazy dog."
},
"equivalent_three": {
"type": "string",
"value": "The quick brown fox jumps over the lazy dog."
}
}

View file

@ -0,0 +1,23 @@
multiline_empty_one = """"""
multiline_empty_two = """
"""
multiline_empty_three = """\
"""
multiline_empty_four = """\
\
\
"""
equivalent_one = "The quick brown fox jumps over the lazy dog."
equivalent_two = """
The quick brown \
fox jumps over \
the lazy dog."""
equivalent_three = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""

View file

@ -0,0 +1,30 @@
{
"backspace": {
"type": "string",
"value": "This string has a \u0008 backspace character."
},
"tab": {
"type": "string",
"value": "This string has a \u0009 tab character."
},
"newline": {
"type": "string",
"value": "This string has a \u000A new line character."
},
"formfeed": {
"type": "string",
"value": "This string has a \u000C form feed character."
},
"carriage": {
"type": "string",
"value": "This string has a \u000D carriage return character."
},
"quote": {
"type": "string",
"value": "This string has a \u0022 quote character."
},
"backslash": {
"type": "string",
"value": "This string has a \u005C backslash character."
}
}

View file

@ -0,0 +1,7 @@
backspace = "This string has a \b backspace character."
tab = "This string has a \t tab character."
newline = "This string has a \n new line character."
formfeed = "This string has a \f form feed character."
carriage = "This string has a \r carriage return character."
quote = "This string has a \" quote character."
backslash = "This string has a \\ backslash character."

View file

@ -0,0 +1,182 @@
# Comment
# I am a comment. Hear me roar. Roar.
# Table
# Tables (also known as hash tables or dictionaries) are collections of key/value pairs.
# They appear in square brackets on a line by themselves.
[Table]
key = "value" # Yeah, you can do this.
# Nested tables are denoted by table names with dots in them. Name your tables whatever crap you please, just don't use #, ., [ or ].
[dog.tater]
type = "pug"
# You don't need to specify all the super-tables if you don't want to. TOML knows how to do it for you.
# [x] you
# [x.y] don't
# [x.y.z] need these
[x.y.z.w] # for this to work
# String
# There are four ways to express strings: basic, multi-line basic, literal, and multi-line literal.
# All strings must contain only valid UTF-8 characters.
[String]
basic = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."
[String.Multiline]
# The following strings are byte-for-byte equivalent:
key1 = "One\nTwo"
key2 = """One\nTwo"""
key3 = """
One
Two"""
[String.Multilined.Singleline]
# The following strings are byte-for-byte equivalent:
key1 = "The quick brown fox jumps over the lazy dog."
key2 = """
The quick brown \
fox jumps over \
the lazy dog."""
key3 = """\
The quick brown \
fox jumps over \
the lazy dog.\
"""
[String.Literal]
# What you see is what you get.
winpath = 'C:\Users\nodejs\templates'
winpath2 = '\\ServerX\admin$\system32\'
quoted = 'Tom "Dubs" Preston-Werner'
regex = '<\i\c*\s*>'
[String.Literal.Multiline]
regex2 = '''I [dw]on't need \d{2} apples'''
lines = '''
The first newline is
trimmed in raw strings.
All other whitespace
is preserved.
'''
# Integer
# Integers are whole numbers. Positive numbers may be prefixed with a plus sign.
# Negative numbers are prefixed with a minus sign.
[Integer]
key1 = +99
key2 = 42
key3 = 0
key4 = -17
# Float
# A float consists of an integer part (which may be prefixed with a plus or minus sign)
# followed by a fractional part and/or an exponent part.
[Float.fractional]
# fractional
key1 = +1.0
key2 = 3.1415
key3 = -0.01
[Float.exponent]
# exponent
key1 = 5e+22
key2 = 1e6
key3 = -2E-2
[Float.both]
# both
key = 6.626e-34
# Boolean
# Booleans are just the tokens you're used to. Always lowercase.
[Booleans]
True = true
False = false
# Datetime
# Datetimes are RFC 3339 dates.
[Datetime]
key1 = 1979-05-27T07:32:00Z
key2 = 1979-05-27T00:32:00-07:00
key3 = 1979-05-27T00:32:00.999-07:00
# Array
# Arrays are square brackets with other primitives inside. Whitespace is ignored. Elements are separated by commas. Data types may not be mixed.
[Array]
key1 = [ 1, 2, 3 ]
key2 = [ "red", "yellow", "green" ]
key3 = [ [ 1, 2 ], [3, 4, 5] ]
key4 = [ [ 1, 2 ], ["a", "b", "c"] ] # this is ok
#Arrays can also be multiline. So in addition to ignoring whitespace, arrays also ignore newlines between the brackets.
# Terminating commas are ok before the closing bracket.
key5 = [
1, 2, 3
]
key6 = [
1,
2, # this is ok
]
# Array of Tables
# These can be expressed by using a table name in double brackets.
# Each table with the same double bracketed name will be an element in the array.
# The tables are inserted in the order encountered.
[[products]]
name = "Hammer"
sku = 738594937
[[products]]
[[products]]
name = "Nail"
sku = 284758393
color = "gray"
# You can create nested arrays of tables as well.
[[fruit]]
name = "apple"
[fruit.physical]
color = "red"
shape = "round"
[[fruit.variety]]
name = "red delicious"
[[fruit.variety]]
name = "granny smith"
[[fruit]]
name = "banana"
[[fruit.variety]]
name = "plantain"

View file

@ -13,8 +13,8 @@ test_string = "You'll hate me after this - #" # " Annoying, isn't it?
harder_test_string = " And when \"'s are in the string, along with # \"" # "and comments are there too"
# Things will get harder
[the.hard.bit#]
what? = "You don't think some user won't do that?"
[the.hard."bit#"]
"what?" = "You don't think some user won't do that?"
multi_line_array = [
"]",
# ] Oh yes I did

View file

@ -0,0 +1,5 @@
points = [ { x = 1, y = 2, z = 3 },
{ x = 7, y = 8, z = 9 },
{ x = 2, y = 4, z = 8 },
{ x = "3", y = "6", z = "12" }
]

View file

@ -1,16 +0,0 @@
a = 1
b.c = 2
[b]
c = 3
a.b = 4
[d.e]
a = 5
a.b.c = 6
[[f]]
a.b = 7
[[f]]
a.b = 8

View file

@ -1,3 +1,3 @@
key = "\" \t \n \r \\ \/ \b \f"
key = "\" \t \n \r \\ \b \f"
unicode_key = "more or less \u00B1"
unicode_key_uppercase = "more or less \U00B1"