From e7d7de7ae5d5a0b7e6d59e095d4ee9bae829ad68 Mon Sep 17 00:00:00 2001 From: Jonathan Wood Date: Sun, 31 May 2015 21:08:42 -0700 Subject: [PATCH] Implement serialization. This provides functionality to convert populated Toml instances and arbitrary objects into TOML. --- README.md | 28 ++ .../moandjiezana/toml/ArraySerializer.java | 69 +++++ .../moandjiezana/toml/BooleanSerializer.java | 25 ++ .../com/moandjiezana/toml/DateSerializer.java | 29 ++ .../com/moandjiezana/toml/MapSerializer.java | 98 ++++++ .../moandjiezana/toml/NumberSerializer.java | 25 ++ .../moandjiezana/toml/ObjectSerializer.java | 76 +++++ .../toml/PrimitiveArraySerializer.java | 28 ++ .../com/moandjiezana/toml/Serializer.java | 11 + .../moandjiezana/toml/SerializerContext.java | 50 ++++ .../com/moandjiezana/toml/Serializers.java | 38 +++ .../moandjiezana/toml/StringSerializer.java | 56 ++++ .../toml/TableArraySerializer.java | 23 ++ src/main/java/com/moandjiezana/toml/Toml.java | 42 ++- .../com/moandjiezana/toml/SerializerTest.java | 281 ++++++++++++++++++ 15 files changed, 863 insertions(+), 16 deletions(-) create mode 100644 src/main/java/com/moandjiezana/toml/ArraySerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/BooleanSerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/DateSerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/MapSerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/NumberSerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/ObjectSerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/PrimitiveArraySerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/Serializer.java create mode 100644 src/main/java/com/moandjiezana/toml/SerializerContext.java create mode 100644 src/main/java/com/moandjiezana/toml/Serializers.java create mode 100644 src/main/java/com/moandjiezana/toml/StringSerializer.java create mode 100644 src/main/java/com/moandjiezana/toml/TableArraySerializer.java create mode 100644 src/test/java/com/moandjiezana/toml/SerializerTest.java diff --git a/README.md b/README.md index b64de95..5f42ed0 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,34 @@ Long tableD = toml.getLong("table.d"); // returns null, not 5, because of shallo Long arrayD = toml.getLong("array[0].d"); // returns 3 ``` +### Serialization + +You can serialize a `Toml` or any arbitrary object to a TOML string. + +Once you have populated a `Toml` via `Toml.parse()`, you can serialize it back to TOML. + +```java +Toml toml = new Toml().parse("a=1"); +String tomlString = toml.serialize(); +``` + +Or you can serialize any object. + +```java +class AClass { + int anInt = 1; + int[] anArray = { 2, 3 }; +} + +String tomlString = Toml.serializeFrom(new AClass()); + +/* +yields: +anInt = 1 +anArray = [ 2, 3 ] +*/ +``` + ### Limitations Date precision is limited to milliseconds. diff --git a/src/main/java/com/moandjiezana/toml/ArraySerializer.java b/src/main/java/com/moandjiezana/toml/ArraySerializer.java new file mode 100644 index 0000000..c396b3a --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/ArraySerializer.java @@ -0,0 +1,69 @@ +package com.moandjiezana.toml; + + +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; + +abstract class ArraySerializer implements Serializer { + static protected boolean isArrayish(Object value) { + return value instanceof Collection || value.getClass().isArray(); + } + + @Override + public boolean isPrimitiveType() { + return false; + } + + @Override + public boolean isTable() { + return false; + } + + static boolean isArrayOfPrimitive(Object array) { + Object first = peek(array); + if (first != null) { + Serializer serializer = Serializers.findSerializerFor(first); + return serializer.isPrimitiveType() || isArrayish(first); + } + + return false; + } + + @SuppressWarnings("unchecked") + protected Collection normalize(Object value) { + Collection collection; + + if (value.getClass().isArray()) { + // Arrays.asList() interprets an array as a single element, + // so convert it to a list by hand + collection = new ArrayList(Array.getLength(value)); + for (int i = 0; i < Array.getLength(value); i++) { + Object elem = Array.get(value, i); + collection.add(elem); + } + } else { + collection = (Collection) value; + } + + return collection; + } + + @SuppressWarnings("unchecked") + private static Object peek(Object value) { + if (value.getClass().isArray()) { + if (Array.getLength(value) > 0) { + return Array.get(value, 0); + } else { + return null; + } + } else { + Collection collection = (Collection) value; + if (collection.size() > 0) { + return collection.iterator().next(); + } + } + + return null; + } +} diff --git a/src/main/java/com/moandjiezana/toml/BooleanSerializer.java b/src/main/java/com/moandjiezana/toml/BooleanSerializer.java new file mode 100644 index 0000000..022ac20 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/BooleanSerializer.java @@ -0,0 +1,25 @@ +package com.moandjiezana.toml; + +class BooleanSerializer implements Serializer { + static final Serializer BOOLEAN_SERIALIZER = new BooleanSerializer(); + + @Override + public boolean canSerialize(Object value) { + return Boolean.class.isInstance(value); + } + + @Override + public void serialize(Object value, SerializerContext context) { + context.serialized.append(value.toString()); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + @Override + public boolean isTable() { + return false; + } +} diff --git a/src/main/java/com/moandjiezana/toml/DateSerializer.java b/src/main/java/com/moandjiezana/toml/DateSerializer.java new file mode 100644 index 0000000..f9935b8 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/DateSerializer.java @@ -0,0 +1,29 @@ +package com.moandjiezana.toml; + +import java.text.SimpleDateFormat; +import java.util.Date; + +class DateSerializer implements Serializer { + static final Serializer DATE_SERIALIZER = new DateSerializer(); + private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:m:ssXXX"); + + @Override + public boolean canSerialize(Object value) { + return value instanceof Date; + } + + @Override + public void serialize(Object value, SerializerContext context) { + context.serialized.append(dateFormat.format(value)); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + @Override + public boolean isTable() { + return false; + } +} diff --git a/src/main/java/com/moandjiezana/toml/MapSerializer.java b/src/main/java/com/moandjiezana/toml/MapSerializer.java new file mode 100644 index 0000000..bd7b30e --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/MapSerializer.java @@ -0,0 +1,98 @@ +package com.moandjiezana.toml; + +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static com.moandjiezana.toml.PrimitiveArraySerializer.PRIMITIVE_ARRAY_SERIALIZER; +import static com.moandjiezana.toml.TableArraySerializer.TABLE_ARRAY_SERIALIZER; + +class MapSerializer implements Serializer { + static final Serializer MAP_SERIALIZER = new MapSerializer(); + + private static final Pattern requiredQuotingPattern = Pattern.compile("^.*[^A-Za-z\\d_-].*$"); + + @Override + public boolean canSerialize(Object value) { + return value instanceof Map; + } + + @Override + public void serialize(Object value, SerializerContext context) { + Map from = (Map) value; + + if (hasPrimitiveValues(from)) { + context.serializeKey(); + } + + // Render primitive types and arrays of primitive first so they are + // grouped under the same table (if there is one) + for (Object key : from.keySet()) { + Object fromValue = from.get(key); + if (fromValue == null) { + continue; + } + + Serializer serializer = Serializers.findSerializerFor(fromValue); + if (serializer.isPrimitiveType()) { + context.indent(); + context.serialized.append(quoteKey(key)).append(" = "); + serializer.serialize(fromValue, context); + context.serialized.append('\n'); + } else if (serializer == PRIMITIVE_ARRAY_SERIALIZER) { + context.serialized.append(quoteKey(key)).append(" = "); + serializer.serialize(fromValue, context); + context.serialized.append('\n'); + } + } + + // Now render (sub)tables and arrays of tables + for (Object key : from.keySet()) { + Object fromValue = from.get(key); + if (fromValue == null) { + continue; + } + + Serializer serializer = Serializers.findSerializerFor(fromValue); + if (serializer.isTable() || serializer == TABLE_ARRAY_SERIALIZER) { + serializer.serialize(fromValue, context.extend(quoteKey(key))); + } + } + } + + @Override + public boolean isPrimitiveType() { + return false; + } + + @Override + public boolean isTable() { + return true; + } + + private static String quoteKey(Object key) { + String stringKey = key.toString(); + Matcher matcher = requiredQuotingPattern.matcher(stringKey); + if (matcher.matches()) { + stringKey = "\"" + stringKey + "\""; + } + + return stringKey; + } + + private static boolean hasPrimitiveValues(Map values) { + for (Object key : values.keySet()) { + Object fromValue = values.get(key); + if (fromValue == null) { + continue; + } + + Serializer serializer = Serializers.findSerializerFor(fromValue); + if (serializer.isPrimitiveType() || serializer == PRIMITIVE_ARRAY_SERIALIZER) { + return true; + } + } + + return false; + } +} diff --git a/src/main/java/com/moandjiezana/toml/NumberSerializer.java b/src/main/java/com/moandjiezana/toml/NumberSerializer.java new file mode 100644 index 0000000..78eab87 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/NumberSerializer.java @@ -0,0 +1,25 @@ +package com.moandjiezana.toml; + +class NumberSerializer implements Serializer { + static final Serializer NUMBER_SERIALIZER = new NumberSerializer(); + + @Override + public boolean canSerialize(Object value) { + return Number.class.isInstance(value); + } + + @Override + public void serialize(Object value, SerializerContext context) { + context.serialized.append(value.toString()); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + @Override + public boolean isTable() { + return false; + } +} diff --git a/src/main/java/com/moandjiezana/toml/ObjectSerializer.java b/src/main/java/com/moandjiezana/toml/ObjectSerializer.java new file mode 100644 index 0000000..ffe106c --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/ObjectSerializer.java @@ -0,0 +1,76 @@ +package com.moandjiezana.toml; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.function.Predicate; + +import static com.moandjiezana.toml.MapSerializer.MAP_SERIALIZER; + +class ObjectSerializer implements Serializer { + static final Serializer OBJECT_SERIALIZER = new ObjectSerializer(); + + @Override + public boolean canSerialize(Object value) { + return true; + } + + @Override + public void serialize(Object value, SerializerContext context) { + Map to = new LinkedHashMap(); + Set fields = getFieldsForClass(value.getClass()); + for (Field field : fields) { + to.put(field.getName(), getFieldValue(field, value)); + } + + MAP_SERIALIZER.serialize(to, context); + } + + @Override + public boolean isPrimitiveType() { + return false; + } + + @Override + public boolean isTable() { + return true; + } + + static private Set getFieldsForClass(Class cls) { + Set fields = new LinkedHashSet(Arrays.asList(cls.getDeclaredFields())); + + getSuperClassFields(cls.getSuperclass(), fields); + + // Skip final fields + fields.removeIf(new Predicate() { + @Override + public boolean test(Field field) { + return Modifier.isFinal(field.getModifiers()); + } + }); + + return fields; + } + + static private void getSuperClassFields(Class cls, Set fields) { + if (cls == Object.class) { + return; + } + + fields.addAll(Arrays.asList(cls.getDeclaredFields())); + getSuperClassFields(cls.getSuperclass(), fields); + } + + static private Object getFieldValue(Field field, Object o) { + boolean isAccessible = field.isAccessible(); + field.setAccessible(true); + Object value = null; + try { + value = field.get(o); + } catch (IllegalAccessException ignored) { + } + field.setAccessible(isAccessible); + + return value; + } +} diff --git a/src/main/java/com/moandjiezana/toml/PrimitiveArraySerializer.java b/src/main/java/com/moandjiezana/toml/PrimitiveArraySerializer.java new file mode 100644 index 0000000..79be5ab --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/PrimitiveArraySerializer.java @@ -0,0 +1,28 @@ +package com.moandjiezana.toml; + +import java.util.Collection; + +class PrimitiveArraySerializer extends ArraySerializer { + static final Serializer PRIMITIVE_ARRAY_SERIALIZER = new PrimitiveArraySerializer(); + + @Override + public boolean canSerialize(Object value) { + return isArrayish(value) && isArrayOfPrimitive(value); + } + + @Override + public void serialize(Object value, SerializerContext context) { + Collection values = normalize(value); + + context.serialized.append("[ "); + boolean first = true; + for (Object elem : values) { + if (!first) { + context.serialized.append(", "); + } + Serializers.serialize(elem, context); + first = false; + } + context.serialized.append(" ]"); + } +} diff --git a/src/main/java/com/moandjiezana/toml/Serializer.java b/src/main/java/com/moandjiezana/toml/Serializer.java new file mode 100644 index 0000000..c0ef999 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/Serializer.java @@ -0,0 +1,11 @@ +package com.moandjiezana.toml; + +interface Serializer { + boolean canSerialize(Object value); + + void serialize(Object value, SerializerContext context); + + boolean isPrimitiveType(); + + boolean isTable(); +} diff --git a/src/main/java/com/moandjiezana/toml/SerializerContext.java b/src/main/java/com/moandjiezana/toml/SerializerContext.java new file mode 100644 index 0000000..a83434b --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/SerializerContext.java @@ -0,0 +1,50 @@ +package com.moandjiezana.toml; + +class SerializerContext { + private String key = ""; + private boolean isArrayOfTable = false; + StringBuilder serialized = new StringBuilder(); + + SerializerContext(String key, StringBuilder serialized) { + this.key = key; + this.serialized = serialized; + } + + SerializerContext() { + } + + SerializerContext extend(String newKey) { + String fullKey = key + (key.isEmpty() ? newKey : "." + newKey); + + return new SerializerContext(fullKey, serialized); + } + + SerializerContext extend() { + return new SerializerContext(key, serialized); + } + + void serializeKey() { + if (key.isEmpty()) { + return; + } + + if (serialized.length() > 0) { + serialized.append('\n'); + } + + if (isArrayOfTable) { + serialized.append("[[").append(key).append("]]\n"); + } else { + serialized.append('[').append(key).append("]\n"); + } + } + + void indent() { + serialized.append(key.isEmpty() ? "" : " "); + } + + SerializerContext setIsArrayOfTable(boolean isArrayOfTable) { + this.isArrayOfTable = isArrayOfTable; + return this; + } +} diff --git a/src/main/java/com/moandjiezana/toml/Serializers.java b/src/main/java/com/moandjiezana/toml/Serializers.java new file mode 100644 index 0000000..fb49757 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/Serializers.java @@ -0,0 +1,38 @@ +package com.moandjiezana.toml; + +import static com.moandjiezana.toml.BooleanSerializer.BOOLEAN_SERIALIZER; +import static com.moandjiezana.toml.DateSerializer.DATE_SERIALIZER; +import static com.moandjiezana.toml.MapSerializer.MAP_SERIALIZER; +import static com.moandjiezana.toml.NumberSerializer.NUMBER_SERIALIZER; +import static com.moandjiezana.toml.ObjectSerializer.OBJECT_SERIALIZER; +import static com.moandjiezana.toml.PrimitiveArraySerializer.PRIMITIVE_ARRAY_SERIALIZER; +import static com.moandjiezana.toml.StringSerializer.STRING_SERIALIZER; +import static com.moandjiezana.toml.TableArraySerializer.TABLE_ARRAY_SERIALIZER; + +abstract class Serializers { + private static final Serializer[] SERIALIZERS = { + STRING_SERIALIZER, NUMBER_SERIALIZER, BOOLEAN_SERIALIZER, DATE_SERIALIZER, + MAP_SERIALIZER, PRIMITIVE_ARRAY_SERIALIZER, TABLE_ARRAY_SERIALIZER + }; + + static Serializer findSerializerFor(Object value) { + for (Serializer serializer : SERIALIZERS) { + if (serializer.canSerialize(value)) { + return serializer; + } + } + + return OBJECT_SERIALIZER; + } + + static String serialize(Object value) { + SerializerContext context = new SerializerContext(); + serialize(value, context); + + return context.serialized.toString(); + } + + static void serialize(Object value, SerializerContext context) { + findSerializerFor(value).serialize(value, context); + } +} diff --git a/src/main/java/com/moandjiezana/toml/StringSerializer.java b/src/main/java/com/moandjiezana/toml/StringSerializer.java new file mode 100644 index 0000000..d3eee57 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/StringSerializer.java @@ -0,0 +1,56 @@ +package com.moandjiezana.toml; + +class StringSerializer implements Serializer { + static final Serializer STRING_SERIALIZER = new StringSerializer(); + + static private final String[] specialCharacterEscapes = new String[93]; + + static { + specialCharacterEscapes[0x08] = "\\b"; + specialCharacterEscapes[0x09] = "\\t"; + specialCharacterEscapes[0x0A] = "\\n"; + specialCharacterEscapes[0x0C] = "\\f"; + specialCharacterEscapes[0x0D] = "\\r"; + specialCharacterEscapes[0x22] = "\\\""; + specialCharacterEscapes[0x5C] = "\\"; + } + + @Override + public boolean canSerialize(Object value) { + return value.getClass().isAssignableFrom(String.class); + } + + @Override + public void serialize(Object value, SerializerContext context) { + context.serialized.append('"'); + escapeUnicode(value.toString(), context.serialized); + context.serialized.append('"'); + } + + @Override + public boolean isPrimitiveType() { + return true; + } + + @Override + public boolean isTable() { + return false; + } + + private void escapeUnicode(String in, StringBuilder out) { + for (int i = 0; i < in.length(); i++) { + int codePoint = in.codePointAt(i); + if (codePoint < specialCharacterEscapes.length && specialCharacterEscapes[codePoint] != null) { + out.append(specialCharacterEscapes[codePoint]); + } else if (codePoint > 0x1f && codePoint < 0x7f) { + out.append(Character.toChars(codePoint)); + } else if (codePoint <= 0xFFFF) { + out.append(String.format("\\u%04X", codePoint)); + } else { + out.append(String.format("\\U%08X", codePoint)); + // Skip the low surrogate, which will be the next in the code point sequence. + i++; + } + } + } +} diff --git a/src/main/java/com/moandjiezana/toml/TableArraySerializer.java b/src/main/java/com/moandjiezana/toml/TableArraySerializer.java new file mode 100644 index 0000000..d2c2912 --- /dev/null +++ b/src/main/java/com/moandjiezana/toml/TableArraySerializer.java @@ -0,0 +1,23 @@ +package com.moandjiezana.toml; + +import java.util.Collection; + +class TableArraySerializer extends ArraySerializer { + static final Serializer TABLE_ARRAY_SERIALIZER = new TableArraySerializer(); + + @Override + public boolean canSerialize(Object value) { + return isArrayish(value) && !isArrayOfPrimitive(value); + } + + @Override + public void serialize(Object value, SerializerContext context) { + Collection values = normalize(value); + + SerializerContext subContext = context.extend().setIsArrayOfTable(true); + + for (Object elem : values) { + Serializers.serialize(elem, subContext); + } + } +} diff --git a/src/main/java/com/moandjiezana/toml/Toml.java b/src/main/java/com/moandjiezana/toml/Toml.java index fd14171..659ad87 100644 --- a/src/main/java/com/moandjiezana/toml/Toml.java +++ b/src/main/java/com/moandjiezana/toml/Toml.java @@ -1,24 +1,11 @@ package com.moandjiezana.toml; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.ArrayList; -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 com.google.gson.Gson; import com.google.gson.JsonElement; +import java.io.*; +import java.util.*; + /** *

Provides access to the keys and tables in a TOML data source.

* @@ -282,6 +269,29 @@ public class Toml { return DEFAULT_GSON.fromJson(json, targetClass); } + /** + * Serializes the values of this Toml instance into TOML. + * + * @return a string containing the TOML representation of this Toml instance. + */ + public String serialize() { + return Serializers.serialize(values); + } + + /** + * Serializes an Object into TOML. + * + * The input can comprise arbitrarily nested combinations of Java primitive types, + * other {@link Object}s, {@link Map}s, {@link List}s, and Arrays. {@link Object}s and {@link Map}s + * are serialized to TOML tables, and {@link List}s and Array to TOML arrays. + * + * @param from the object to be serialized + * @return a string containing the TOML representation of the given Object + */ + public static String serializeFrom(Object from) { + return Serializers.serialize(from); + } + @SuppressWarnings("unchecked") private Object get(String key) { if (values.containsKey(key)) { diff --git a/src/test/java/com/moandjiezana/toml/SerializerTest.java b/src/test/java/com/moandjiezana/toml/SerializerTest.java new file mode 100644 index 0000000..444d0d4 --- /dev/null +++ b/src/test/java/com/moandjiezana/toml/SerializerTest.java @@ -0,0 +1,281 @@ +package com.moandjiezana.toml; + +import org.junit.Test; + +import java.io.UnsupportedEncodingException; +import java.util.*; + +import static org.junit.Assert.assertEquals; + +public class SerializerTest { + @Test + public void serializesPrimitiveTypes() { + class TestClass { + public String aString; + int anInt; + protected float aFloat; + private double aDouble; + boolean aBoolean; + final int aFinalInt = 1; // Should be skipped + Date aDate; + } + + TestClass o = new TestClass(); + o.aString = "hello"; + o.anInt = 4; + o.aFloat = 1.23f; + o.aDouble = -5.43; + o.aBoolean = false; + + String theDate = "2015-05-31T08:44:03-07:00"; + Toml dateToml = new Toml().parse("a_date = " + theDate); + o.aDate = dateToml.getDate("a_date"); + + String serialized = Toml.serializeFrom(o); + String expected = "aString = \"hello\"\n" + + "anInt = 4\n" + + "aFloat = 1.23\n" + + "aDouble = -5.43\n" + + "aBoolean = false\n" + + "aDate = " + theDate + "\n"; + + assertEquals(expected, serialized); + } + + @Test + public void serializesNestedMap() { + class SubChild { + int anInt; + } + class Child { + SubChild subChild; + int anInt; + } + class Parent { + Map aMap; + Child child; + boolean aBoolean; + } + + Parent parent = new Parent(); + parent.aMap = new LinkedHashMap(); + parent.aMap.put("foo", 1); + parent.aMap.put("bar", "value1"); + parent.aMap.put("baz.x", true); + parent.child = new Child(); + parent.child.anInt = 2; + parent.child.subChild = new SubChild(); + parent.child.subChild.anInt = 4; + parent.aBoolean = true; + + String serialized = Toml.serializeFrom(parent); + String expected = "aBoolean = true\n\n" + + "[aMap]\n" + + " foo = 1\n" + + " bar = \"value1\"\n" + + " \"baz.x\" = true\n\n" + + "[child]\n" + + " anInt = 2\n\n" + + "[child.subChild]\n" + + " anInt = 4\n"; + assertEquals(expected, serialized); + } + + @Test + public void serializesArrayOfPrimitive() { + class ArrayTest { + int[] array = {1, 2, 3}; + } + + ArrayTest arrayTest = new ArrayTest(); + String serialized = Toml.serializeFrom(arrayTest); + String expected = "array = [ 1, 2, 3 ]\n"; + assertEquals(expected, serialized); + } + + @Test + public void serializesArrayOfTables() { + class Table { + int anInt; + + Table(int anInt) { + this.anInt = anInt; + } + } + class Config { + Table[] table; + } + Config config = new Config(); + config.table = new Table[]{new Table(1), new Table(2)}; + + String serialized = Toml.serializeFrom(config); + String expected = "[[table]]\n" + + " anInt = 1\n\n" + + "[[table]]\n" + + " anInt = 2\n"; + assertEquals(expected, serialized); + } + + @Test + public void serializesArrayOfArray() { + class ArrayTest { + int[][] array = {{1, 2, 3}, {4, 5, 6}}; + } + ArrayTest arrayTest = new ArrayTest(); + + String serialized = Toml.serializeFrom(arrayTest); + String expected = "array = [ [ 1, 2, 3 ], [ 4, 5, 6 ] ]\n"; + assertEquals(expected, serialized); + } + + @Test + public void serializesList() { + class ListTest { + List aList = new LinkedList(); + } + ListTest o = new ListTest(); + o.aList.add(1); + o.aList.add(2); + + assertEquals("aList = [ 1, 2 ]\n", Toml.serializeFrom(o)); + } + + @Test + public void handlesZeroLengthArraysAndLists() { + class TestClass { + List aList = new LinkedList(); + Float[] anArray = new Float[0]; + } + assertEquals("", Toml.serializeFrom(new TestClass())); + } + + @Test + public void elidesEmptyIntermediateTables() { + class C { + int anInt = 1; + } + class B { + C c = new C(); + } + class A { + B b = new B(); + } + + assertEquals("[b.c]\n anInt = 1\n", Toml.serializeFrom(new A())); + } + + @Test + public void serializesNestedArraysOfTables() { + class Physical { + String color; + String shape; + } + class Variety { + String name; + } + class Fruit { + Physical physical; + Variety[] variety; + String name; + } + class Basket { + Fruit[] fruit; + } + + Basket basket = new Basket(); + basket.fruit = new Fruit[2]; + + basket.fruit[0] = new Fruit(); + basket.fruit[0].name = "apple"; + basket.fruit[0].physical = new Physical(); + basket.fruit[0].physical.color = "red"; + basket.fruit[0].physical.shape = "round"; + basket.fruit[0].variety = new Variety[2]; + basket.fruit[0].variety[0] = new Variety(); + basket.fruit[0].variety[0].name = "red delicious"; + basket.fruit[0].variety[1] = new Variety(); + basket.fruit[0].variety[1].name = "granny smith"; + + basket.fruit[1] = new Fruit(); + basket.fruit[1].name = "banana"; + basket.fruit[1].variety = new Variety[1]; + basket.fruit[1].variety[0] = new Variety(); + basket.fruit[1].variety[0].name = "plantain"; + + String expected = "[[fruit]]\n" + + " name = \"apple\"\n" + + "\n" + + "[fruit.physical]\n" + + " color = \"red\"\n" + + " shape = \"round\"\n" + + "\n" + + "[[fruit.variety]]\n" + + " name = \"red delicious\"\n" + + "\n" + + "[[fruit.variety]]\n" + + " name = \"granny smith\"\n" + + "\n" + + "[[fruit]]\n" + + " name = \"banana\"\n" + + "\n" + + "[[fruit.variety]]\n" + + " name = \"plantain\"" + + "\n"; + + + String serialized = Toml.serializeFrom(basket); + assertEquals(expected, serialized); + } + + @Test + public void serializesClassesWithInheritance() { + class Parent { + protected int anInt = 2; + } + class Child extends Parent { + boolean aBoolean = true; + } + + Child child = new Child(); + String expected = "aBoolean = true\nanInt = 2\n"; + assertEquals(expected, Toml.serializeFrom(child)); + } + + @Test + public void emptyTomlSerializesToEmptyString() { + Toml toml = new Toml(); + assertEquals("", toml.serialize()); + } + + @Test + public void serializesStringsToTomlUtf8() throws UnsupportedEncodingException { + String input = " é foo € \b \t \n \f \r \" \\ "; + assertEquals("\" \\u00E9 foo \\u20AC \\b \\t \\n \\f \\r \\\" \\ \"", Toml.serializeFrom(input)); + + // Check unicode code points greater than 0XFFFF + input = " \uD801\uDC28 \uD840\uDC0B "; + assertEquals("\" \\U00010428 \\U0002000B \"", Toml.serializeFrom(input)); + } + + @Test + public void quotesKeys() { + Map aMap = new LinkedHashMap(); + aMap.put("a.b", 1); + aMap.put("5€", 2); + aMap.put("c$d", 3); + aMap.put("e/f", 4); + + String expected = "\"a.b\" = 1\n" + + "\"5€\" = 2\n" + + "\"c$d\" = 3\n" + + "\"e/f\" = 4\n"; + assertEquals(expected, Toml.serializeFrom(aMap)); + } + + @Test + public void serializesFromToml() { + String tomlString = "a = 1\n"; + Toml toml = new Toml().parse(tomlString); + assertEquals(tomlString, toml.serialize()); + } +} \ No newline at end of file