Skip to content

Commit

Permalink
Merge pull request #49 from kPYKx7Ddw4n1aIKZ/toml_serializer
Browse files Browse the repository at this point in the history
Toml serializer
  • Loading branch information
cleishm committed Oct 18, 2022
2 parents c9769e3 + d0bf4bf commit 1a22c11
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/main/java/org/tomlj/QuotedStringVisitor.java
Expand Up @@ -68,6 +68,8 @@ public StringBuilder visitEscaped(TomlParser.EscapedContext ctx) {
return builder.append('\\');
}
switch (text.charAt(1)) {
case '\'':
return builder.append('\'');
case '"':
return builder.append('"');
case '\\':
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/org/tomlj/Toml.java
Expand Up @@ -241,6 +241,14 @@ public static StringBuilder tomlEscape(String text) {
out.append("\\'");
continue;
}
if (ch == '\"') {
out.append("\\\"");
continue;
}
if (ch == '\\') {
out.append("\\\\");
continue;
}
if (ch >= 0x20 && ch < 0x7F) {
out.append(ch);
continue;
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/org/tomlj/TomlArray.java
Expand Up @@ -362,4 +362,30 @@ default void toJson(Appendable appendable, JsonOptions... options) throws IOExce
default void toJson(Appendable appendable, EnumSet<JsonOptions> options) throws IOException {
JsonSerializer.toJson(this, appendable, options);
}

/**
* Return a representation of this array using TOML.
*
* @return A TOML representation of this array.
*/
default String toToml() {
StringBuilder builder = new StringBuilder();
try {
toToml(builder);
} catch (IOException e) {
// not reachable
throw new UncheckedIOException(e);
}
return builder.toString();
}

/**
* Append a TOML representation of this array to the appendable output.
*
* @param appendable The appendable output.
* @throws IOException If an IO error occurs.
*/
default void toToml(Appendable appendable) throws IOException {
TomlSerializer.toToml(this, appendable);
}
}
166 changes: 166 additions & 0 deletions src/main/java/org/tomlj/TomlSerializer.java
@@ -0,0 +1,166 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.tomlj;

import static java.util.Objects.requireNonNull;
import static org.tomlj.TomlType.ARRAY;
import static org.tomlj.TomlType.TABLE;
import static org.tomlj.TomlType.typeFor;

import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;

final class TomlSerializer {
private TomlSerializer() {}

static void toToml(TomlTable table, Appendable appendable) throws IOException {
requireNonNull(table);
requireNonNull(appendable);
toToml(table, appendable, -2, "");
}

private static void toToml(TomlTable table, Appendable appendable, int indent, String path) throws IOException {
for (Map.Entry<String, Object> entry : table.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();

key = Toml.tomlEscape(key).toString();
if (!key.matches("[a-zA-Z0-9_-]*")) {
key = "\"" + key + "\"";
}

String newPath = (path.isEmpty() ? "" : path + ".") + key;

Optional<TomlType> tomlType = typeFor(value);
assert tomlType.isPresent();

boolean isTableArray = tomlType.get().equals(ARRAY) && isTableArray((TomlArray) value);

if (tomlType.get().equals(TABLE)) {
append(appendable, indent + 2, "[" + newPath + "]");
appendable.append(System.lineSeparator());
} else if (!isTableArray) {
append(appendable, indent + 2, key + "=");
}

appendTomlValue(value, appendable, indent, newPath);
if (!tomlType.get().equals(TABLE) && !isTableArray) {
appendable.append(System.lineSeparator());
}
}
}

static void toToml(TomlArray array, Appendable appendable) throws IOException {
requireNonNull(array);
requireNonNull(appendable);
toToml(array, appendable, 0, "");
}

private static void toToml(TomlArray array, Appendable appendable, int indent, String path) throws IOException {
boolean tableArray = isTableArray(array);
if (!tableArray) {
appendable.append("[");
if (!array.isEmpty()) {
appendable.append(System.lineSeparator());
}
}

for (Iterator<Object> iterator = array.toList().iterator(); iterator.hasNext();) {
Object tomlValue = iterator.next();
Optional<TomlType> tomlType = typeFor(tomlValue);
assert tomlType.isPresent();
if (tomlType.get().equals(TABLE)) {
append(appendable, indent, "[[" + path + "]]");
appendable.append(System.lineSeparator());
toToml((TomlTable) tomlValue, appendable, indent, path);
} else {
indentLine(appendable, indent + 2);
appendTomlValue(tomlValue, appendable, indent, path);
}

if (!tableArray) {
if (iterator.hasNext()) {
appendable.append(",");
}
appendable.append(System.lineSeparator());
}
}
if (!tableArray) {
append(appendable, indent, "]");
}
}

private static void appendTomlValue(Object value, Appendable appendable, int indent, String path) throws IOException {
Optional<TomlType> tomlType = typeFor(value);
assert tomlType.isPresent();
switch (tomlType.get()) {
case STRING:
append(appendable, 0, "\"" + Toml.tomlEscape((String) value) + "\"");
break;
case INTEGER:
case FLOAT:
append(appendable, 0, value.toString());
break;
case OFFSET_DATE_TIME:
append(appendable, 0, DateTimeFormatter.ISO_OFFSET_DATE_TIME.format((OffsetDateTime) value));
break;
case LOCAL_DATE_TIME:
append(appendable, 0, DateTimeFormatter.ISO_LOCAL_DATE_TIME.format((LocalDateTime) value));
break;
case LOCAL_DATE:
append(appendable, 0, DateTimeFormatter.ISO_LOCAL_DATE.format((LocalDate) value));
break;
case LOCAL_TIME:
append(appendable, 0, DateTimeFormatter.ISO_LOCAL_TIME.format((LocalTime) value));
break;
case BOOLEAN:
append(appendable, 0, ((Boolean) value) ? "true" : "false");
break;
case ARRAY:
toToml((TomlArray) value, appendable, indent + 2, path);
break;
case TABLE:
toToml((TomlTable) value, appendable, indent + 2, path);
break;
}
}

private static void append(Appendable appendable, int indent, String line) throws IOException {
indentLine(appendable, indent);
appendable.append(line);
}

private static void indentLine(Appendable appendable, int indent) throws IOException {
for (int i = 0; i < indent; ++i) {
appendable.append(' ');
}
}

private static boolean isTableArray(TomlArray array) {
for (Object tomlValue : array.toList()) {
Optional<TomlType> tomlType = typeFor(tomlValue);
assert tomlType.isPresent();
if (tomlType.get().equals(TABLE)) {
return true;
}
}
return false;
}
}
26 changes: 26 additions & 0 deletions src/main/java/org/tomlj/TomlTable.java
Expand Up @@ -1268,4 +1268,30 @@ default void toJson(Appendable appendable, JsonOptions... options) throws IOExce
default void toJson(Appendable appendable, EnumSet<JsonOptions> options) throws IOException {
JsonSerializer.toJson(this, appendable, options);
}

/**
* Return a representation of this table using TOML.
*
* @return A TOML representation of this table.
*/
default String toToml() {
StringBuilder builder = new StringBuilder();
try {
toToml(builder);
} catch (IOException e) {
// not reachable
throw new UncheckedIOException(e);
}
return builder.toString();
}

/**
* Append a TOML representation of this table to the appendable output.
*
* @param appendable The appendable output.
* @throws IOException If an IO error occurs.
*/
default void toToml(Appendable appendable) throws IOException {
TomlSerializer.toToml(this, appendable);
}
}
31 changes: 31 additions & 0 deletions src/test/java/org/tomlj/TomlTest.java
Expand Up @@ -19,7 +19,9 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
Expand Down Expand Up @@ -894,6 +896,35 @@ void testArrayInequality() throws Exception {
assertFalse(Toml.equals(array1, array2));
}

void testSerializerArrayTables() throws Exception {
InputStream is = this.getClass().getResourceAsStream("/org/tomlj/array_table_example.toml");
assertNotNull(is);
TomlParseResult result = Toml.parse(is);
assertFalse(result.hasErrors(), () -> joinErrors(result));

String serializedToml = result.toToml();
TomlParseResult resultReparse =
Toml.parse(new ByteArrayInputStream(serializedToml.getBytes(StandardCharsets.UTF_8)));
assertFalse(resultReparse.hasErrors(), () -> joinErrors(result));

assertTrue(Toml.equals(result, resultReparse));
}

@Test
void testSerializerHardExample() throws Exception {
InputStream is = this.getClass().getResourceAsStream("/org/tomlj/hard_example.toml");
assertNotNull(is);
TomlParseResult result = Toml.parse(is);
assertFalse(result.hasErrors(), () -> joinErrors(result));

String serializedToml = result.toToml();
TomlParseResult resultReparse =
Toml.parse(new ByteArrayInputStream(serializedToml.getBytes(StandardCharsets.UTF_8)));
assertFalse(resultReparse.hasErrors(), () -> joinErrors(result));

assertTrue(Toml.equals(result, resultReparse));
}

private String joinErrors(TomlParseResult result) {
return result.errors().stream().map(TomlParseError::toString).collect(Collectors.joining("\n"));
}
Expand Down

0 comments on commit 1a22c11

Please sign in to comment.