diff --git a/src/main/java/io/xlate/yamljson/SnakeYamlEngineGenerator.java b/src/main/java/io/xlate/yamljson/SnakeYamlEngineGenerator.java index 7676f65..4118cd0 100644 --- a/src/main/java/io/xlate/yamljson/SnakeYamlEngineGenerator.java +++ b/src/main/java/io/xlate/yamljson/SnakeYamlEngineGenerator.java @@ -67,14 +67,14 @@ class SnakeYamlEngineGenerator extends YamlGenerator impleme final DumpSettings settings; final Emitter emitter; - SnakeYamlEngineGenerator(DumpSettings settings, YamlWriterStream writer) { - super(STYLES, writer); + SnakeYamlEngineGenerator(Map properties, DumpSettings settings, YamlWriterStream writer) { + super(properties, STYLES, writer); this.settings = settings; this.emitter = new Emitter(settings, writer); } - SnakeYamlEngineGenerator(DumpSettings settings, Writer writer) { - this(settings, new YamlWriterStream(writer)); + SnakeYamlEngineGenerator(Map properties, DumpSettings settings, Writer writer) { + this(properties, settings, new YamlWriterStream(writer)); } @Override diff --git a/src/main/java/io/xlate/yamljson/SnakeYamlGenerator.java b/src/main/java/io/xlate/yamljson/SnakeYamlGenerator.java index 544e65e..d8ac0e9 100644 --- a/src/main/java/io/xlate/yamljson/SnakeYamlGenerator.java +++ b/src/main/java/io/xlate/yamljson/SnakeYamlGenerator.java @@ -65,8 +65,8 @@ class SnakeYamlGenerator extends YamlGenerator implements Js final DumperOptions settings; final Emitter emitter; - SnakeYamlGenerator(DumperOptions settings, Writer writer) { - super(STYLES, writer); + SnakeYamlGenerator(Map properties, DumperOptions settings, Writer writer) { + super(properties, STYLES, writer); this.settings = settings; this.emitter = new Emitter(writer, settings); } diff --git a/src/main/java/io/xlate/yamljson/StringQuotingChecker.java b/src/main/java/io/xlate/yamljson/StringQuotingChecker.java index fcacf70..67e736d 100644 --- a/src/main/java/io/xlate/yamljson/StringQuotingChecker.java +++ b/src/main/java/io/xlate/yamljson/StringQuotingChecker.java @@ -6,7 +6,10 @@ */ class StringQuotingChecker { - StringQuotingChecker() { + private final boolean quoteNumericStrings; + + StringQuotingChecker(boolean quoteNumericStrings) { + this.quoteNumericStrings = quoteNumericStrings; } /** @@ -14,7 +17,7 @@ class StringQuotingChecker { * from being read as non-String key (boolean or number) */ boolean needToQuoteName(String name) { - return isReservedKeyword(name) || YamlNumbers.isFloat(name); + return needToQuote(name, true); } /** @@ -22,8 +25,13 @@ boolean needToQuoteName(String name) { * from being value of different type (boolean or number). */ boolean needToQuoteValue(String value) { - // Only consider reserved keywords but not numbers? - return isReservedKeyword(value) || valueHasQuotableChar(value); + return needToQuote(value, quoteNumericStrings); + } + + boolean needToQuote(String value, boolean quoteNumeric) { + return isReservedKeyword(value) || + hasQuoteableCharacter(value) || + (quoteNumeric && YamlNumbers.isNumeric(value)); } /** @@ -67,7 +75,7 @@ boolean isReservedKeyword(String value) { * quoted in case they contain one of the following characters or character * combinations. */ - boolean valueHasQuotableChar(String inputStr) { + boolean hasQuoteableCharacter(String inputStr) { final int end = inputStr.length(); for (int i = 0; i < end; ++i) { switch (inputStr.charAt(i)) { diff --git a/src/main/java/io/xlate/yamljson/Yaml.java b/src/main/java/io/xlate/yamljson/Yaml.java index 9574a57..f8d965f 100644 --- a/src/main/java/io/xlate/yamljson/Yaml.java +++ b/src/main/java/io/xlate/yamljson/Yaml.java @@ -91,6 +91,8 @@ private Versions() { */ public static final class Settings { + private static final String PRE = "io.xlate.yamljson."; + private Settings() { } @@ -111,7 +113,7 @@ private Settings() { * @see Versions#V1_1 * @see Versions#V1_2 */ - public static final String YAML_VERSION = "io.xlate.yamljson.YAML_VERSION"; + public static final String YAML_VERSION = PRE + "YAML_VERSION"; /** * The maximum number of scalars to which an alias (or chain of aliases via arrays/objects) @@ -177,7 +179,7 @@ private Settings() { * * @since 0.2 */ - public static final String LOAD_CONFIG = "io.xlate.yamljson.LOAD_CONFIG"; + public static final String LOAD_CONFIG = PRE + "LOAD_CONFIG"; /** * Used to pass a pre-configured @@ -187,8 +189,52 @@ private Settings() { * * @since 0.2 */ - public static final String DUMP_CONFIG = "io.xlate.yamljson.DUMP_CONFIG"; + public static final String DUMP_CONFIG = PRE + "DUMP_CONFIG"; + /** + * Whether strings will be rendered without quotes (true) or with quotes + * (false, default). + *

+ * Minimized quote usage makes for more human readable output; however, + * content is limited to printable characters according to the rules of + * literal + * block style. + * + * @since 0.2 + */ + public static final String DUMP_MINIMIZE_QUOTES = PRE + "DUMP_MINIMIZE_QUOTES"; + + /** + * Whether numeric values stored as strings will be rendered with quotes + * (true, default) or without quotes (false) when + * {@link #DUMP_MINIMIZE_QUOTES} is enabled. + * + * @since 0.2 + */ + public static final String DUMP_QUOTE_NUMERIC_STRINGS = PRE + "DUMP_QUOTE_NUMERIC_STRINGS"; + + /** + * Whether strings containing newlines should use literal + * block style. This automatically enabled when + * {@link #DUMP_MINIMIZE_QUOTES} is set. + * + * @since 0.2 + */ + public static final String DUMP_LITERAL_BLOCK_STYLE = PRE + "DUMP_LITERAL_BLOCK_STYLE"; + + /** + * Feature that determines whether {@link java.math.BigDecimal} entries are + * serialized using {@link java.math.BigDecimal#toPlainString()} to prevent + * values to be written using scientific notation. + *

+ * Feature is disabled by default, so default output mode is used; this generally + * depends on how {@link java.math.BigDecimal} has been created. + * + * @since 0.2 + */ + public static final String DUMP_WRITE_PLAIN_BIGDECIMAL = PRE + "DUMP_WRITE_PLAIN_BIGDECIMAL"; } private static final YamlProvider PROVIDER = new YamlProvider(); diff --git a/src/main/java/io/xlate/yamljson/YamlGenerator.java b/src/main/java/io/xlate/yamljson/YamlGenerator.java index 0c29ef7..55b367a 100644 --- a/src/main/java/io/xlate/yamljson/YamlGenerator.java +++ b/src/main/java/io/xlate/yamljson/YamlGenerator.java @@ -68,16 +68,34 @@ interface IOOperation { } static final String VALUE = "value"; + static final String FALSE = "false"; + static final String TRUE = "true"; - static final StringQuotingChecker quoteChecker = new StringQuotingChecker(); - + protected final Map properties; protected final Map styleTypes; protected final Writer writer; - final Deque context = new ArrayDeque<>(); - YamlGenerator(Map styleTypes, Writer writer) { + private final Deque context = new ArrayDeque<>(); + private final boolean minimizeQuotes; + private final boolean quoteNumericStrings; + private final boolean literalBlockStyle; + private final boolean writePlainBigDecimal; + private final StringQuotingChecker quoteChecker; + + YamlGenerator(Map properties, Map styleTypes, Writer writer) { + this.properties = properties; this.styleTypes = styleTypes; this.writer = writer; + this.minimizeQuotes = parse(properties, Yaml.Settings.DUMP_MINIMIZE_QUOTES, FALSE); + this.quoteNumericStrings = parse(properties, Yaml.Settings.DUMP_QUOTE_NUMERIC_STRINGS, TRUE); + this.literalBlockStyle = parse(properties, Yaml.Settings.DUMP_LITERAL_BLOCK_STYLE, FALSE); + this.writePlainBigDecimal = parse(properties, Yaml.Settings.DUMP_WRITE_PLAIN_BIGDECIMAL, FALSE); + this.quoteChecker = new StringQuotingChecker(quoteNumericStrings); + } + + static boolean parse(Map properties, String key, String defaultValue) { + Object value = properties.getOrDefault(key, defaultValue); + return Boolean.parseBoolean(String.valueOf(value)); } protected abstract E getEvent(EventType type); @@ -122,23 +140,22 @@ void emitScalar(Object value, boolean forcePlain, Predicate quoteCheck) style = styleTypes.get(StyleType.PLAIN); } else { scalarValue = String.valueOf(value); - boolean containsNewLine = scalarValue.indexOf('\n') > -1; - boolean containsDoubleQuote = scalarValue.indexOf('"') > -1; - boolean containsSingleQuote = scalarValue.indexOf('\'') > -1; - - if (containsNewLine) { - // XXX: Allow for folded scalar style via configuration - style = styleTypes.get(StyleType.LITERAL); - } else if (containsDoubleQuote && containsSingleQuote) { - style = styleTypes.get(StyleType.LITERAL); - } else if (containsDoubleQuote) { - style = styleTypes.get(StyleType.SINGLE_QUOTED); - } else if (containsSingleQuote) { - style = styleTypes.get(StyleType.DOUBLE_QUOTED); - } else if (quoteCheck.test(scalarValue)) { - style = styleTypes.get(StyleType.SINGLE_QUOTED); + + if (minimizeQuotes) { + if (scalarValue.indexOf('\n') >= 0) { + style = styleTypes.get(StyleType.LITERAL); + } else if (quoteCheck.test(scalarValue)) { + // Preserve quotes for keywords, indicators, and numeric strings (if configured) + style = styleTypes.get(StyleType.DOUBLE_QUOTED); + } else { + style = styleTypes.get(StyleType.PLAIN); + } } else { - style = styleTypes.get(StyleType.PLAIN); + if (literalBlockStyle && scalarValue.indexOf('\n') >= 0) { + style = styleTypes.get(StyleType.LITERAL); + } else { + style = styleTypes.get(StyleType.DOUBLE_QUOTED); + } } } @@ -329,7 +346,8 @@ public JsonGenerator write(String value) { @Override public JsonGenerator write(BigDecimal value) { Objects.requireNonNull(value, VALUE); - emitScalar(value); + Object stringValue = writePlainBigDecimal ? value.toPlainString() : value.toString(); + emitScalar(stringValue); return this; } diff --git a/src/main/java/io/xlate/yamljson/YamlGeneratorFactory.java b/src/main/java/io/xlate/yamljson/YamlGeneratorFactory.java index f441ac6..cf196c5 100644 --- a/src/main/java/io/xlate/yamljson/YamlGeneratorFactory.java +++ b/src/main/java/io/xlate/yamljson/YamlGeneratorFactory.java @@ -109,11 +109,11 @@ public JsonGenerator createGenerator(Writer writer) { if (useSnakeYamlEngine) { var settings = (org.snakeyaml.engine.v2.api.DumpSettings) this.snakeYamlSettings; - return new SnakeYamlEngineGenerator(settings, writer); + return new SnakeYamlEngineGenerator(properties, settings, writer); } var settings = (org.yaml.snakeyaml.DumperOptions) this.snakeYamlSettings; - return new SnakeYamlGenerator(settings, writer); + return new SnakeYamlGenerator(properties, settings, writer); } @Override diff --git a/src/main/java/io/xlate/yamljson/YamlNumbers.java b/src/main/java/io/xlate/yamljson/YamlNumbers.java index 9867d12..831f811 100644 --- a/src/main/java/io/xlate/yamljson/YamlNumbers.java +++ b/src/main/java/io/xlate/yamljson/YamlNumbers.java @@ -25,6 +25,14 @@ final class YamlNumbers { private YamlNumbers() { } + static boolean isNumeric(String dataText) { + return isFloat(dataText) || isSpecial(dataText); + } + + static boolean isSpecial(String dataText) { + return YamlParser.VALUES_INFINITY.contains(dataText) || YamlParser.VALUES_NAN.contains(dataText); + } + static boolean isInteger(String dataText) { final int start; diff --git a/src/test/java/io/xlate/yamljson/YamlGeneratorTest.java b/src/test/java/io/xlate/yamljson/YamlGeneratorTest.java index 26c72c5..419ba83 100644 --- a/src/test/java/io/xlate/yamljson/YamlGeneratorTest.java +++ b/src/test/java/io/xlate/yamljson/YamlGeneratorTest.java @@ -21,21 +21,23 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.ByteArrayOutputStream; +import java.io.OutputStream; import java.io.StringWriter; +import java.io.Writer; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Map; +import jakarta.json.stream.JsonGenerationException; +import jakarta.json.stream.JsonGenerator; +import jakarta.json.stream.JsonGeneratorFactory; + import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.snakeyaml.engine.v2.api.DumpSettings; import org.yaml.snakeyaml.DumperOptions; -import jakarta.json.stream.JsonGenerationException; -import jakarta.json.stream.JsonGenerator; -import jakarta.json.stream.JsonGeneratorFactory; - @DisabledIfSystemProperty(named = Yaml.Settings.YAML_VERSION, matches = "NONE") class YamlGeneratorTest { @@ -52,6 +54,25 @@ void testSimple(String version) { writer.flush(); } + assertEquals("\"testKey\": \"testValue\"\n", writer.toString()); + } + + @ParameterizedTest + @MethodSource(VERSIONS_SOURCE) + void testSimpleWithQuotesMinimized(String version) { + StringWriter writer = new StringWriter(); + Map config = Map.ofEntries( + Map.entry(Yaml.Settings.YAML_VERSION, version), + Map.entry(Yaml.Settings.DUMP_MINIMIZE_QUOTES, true)); + + try (JsonGenerator generator = createGenerator(config, writer)) { + generator.writeStartObject() + .write("testKey", "testValue") + .writeEnd(); + + writer.flush(); + } + assertEquals("testKey: testValue\n", writer.toString()); } @@ -82,8 +103,78 @@ void testWriteKeyInArrayThrowsException(String version) { @MethodSource(VERSIONS_SOURCE) void testSequenceOfValues(String version) { StringWriter writer = new StringWriter(); + writeSequenceOfValues(null, version, writer); + assertEquals("\"values\":\n" + + "- 3.14\n" + + "- 1000000\n" + + "- false\n" + + "- 2.71\n" + + "- 2021\n" + + "- 2022\n" + + "- \"Just a String\"\n" + + "- null\n" + + "- \"This line\\nspans multiple\\nlines\"\n" + + "- \"Contains both ' and \\\" (quote types)\"\n" + + "- \"Contains only '\"\n" + + "- \"Contains only \\\"\"\n" + + "- \"3.14\"\n", + writer.toString()); + } - try (JsonGenerator generator = createGenerator(version, writer)) { + @ParameterizedTest + @MethodSource(VERSIONS_SOURCE) + void testSequenceOfValuesWithLiteralBlockStyle(String version) { + StringWriter writer = new StringWriter(); + Map config = Map.ofEntries( + Map.entry(Yaml.Settings.YAML_VERSION, version), + Map.entry(Yaml.Settings.DUMP_LITERAL_BLOCK_STYLE, true)); + writeSequenceOfValues(config, null, writer); + assertEquals("\"values\":\n" + + "- 3.14\n" + + "- 1000000\n" + + "- false\n" + + "- 2.71\n" + + "- 2021\n" + + "- 2022\n" + + "- \"Just a String\"\n" + + "- null\n" + + "- |-\n This line\n spans multiple\n lines\n" + + "- \"Contains both ' and \\\" (quote types)\"\n" + + "- \"Contains only '\"\n" + + "- \"Contains only \\\"\"\n" + + "- \"3.14\"\n", + writer.toString()); + } + + @ParameterizedTest + @MethodSource(VERSIONS_SOURCE) + void testSequenceOfValuesWithQuotesMinimized(String version) { + StringWriter writer = new StringWriter(); + Map config = Map.ofEntries( + Map.entry(Yaml.Settings.YAML_VERSION, version), + Map.entry(Yaml.Settings.DUMP_MINIMIZE_QUOTES, true)); + writeSequenceOfValues(config, null, writer); + assertEquals("values:\n" + + "- 3.14\n" + + "- 1000000\n" + + "- false\n" + + "- 2.71\n" + + "- 2021\n" + + "- 2022\n" + + "- Just a String\n" + + "- null\n" + + "- |-\n This line\n spans multiple\n lines\n" + + "- Contains both ' and \" (quote types)\n" + + "- Contains only '\n" + + "- Contains only \"\n" + + "- \"3.14\"\n", + writer.toString()); + } + + private void writeSequenceOfValues(Map config, String version, Writer writer) { + try (JsonGenerator generator = config != null ? + createGenerator(config, writer) : + createGenerator(version, writer)) { generator.writeStartObject() .writeStartArray("values") .write(new BigDecimal("3.14")) @@ -98,32 +189,73 @@ void testSequenceOfValues(String version) { .write("Contains both ' and \" (quote types)") .write("Contains only '") .write("Contains only \"") + .write("3.14") // string that looks like a number .writeEnd() .writeEnd(); } - - assertEquals("values:\n" - + "- 3.14\n" - + "- 1000000\n" - + "- false\n" - + "- 2.71\n" - + "- 2021\n" - + "- 2022\n" - + "- Just a String\n" - + "- null\n" - + "- |-\n This line\n spans multiple\n lines\n" - + "- |-\n Contains both ' and \" (quote types)\n" - + "- \"Contains only '\"\n" - + "- 'Contains only \"'\n", - writer.toString()); } @ParameterizedTest @MethodSource(VERSIONS_SOURCE) void testMappingOfValues(String version) { ByteArrayOutputStream stream = new ByteArrayOutputStream(); + writeMappingOfValues(null, version, stream); + assertEquals("\"values\":\n" + + " \"BigDecimal\": 3.14\n" + + " \"BigInteger\": 1000000\n" + + " \"Boolean\": true\n" + + " \"double\": 2.71\n" + + " \"int\": 2021\n" + + " \"long\": 2022\n" + + " \"String\": \"Just a String\"\n" + + " \"Null\": null\n" + + " \"Multiline\": \"This line\\nspans multiple\\nlines\"\n" + + " \"MultilineQuotes\": \"Contains both ' and \\\" (quote types)\"\n" + + " \"SingleQuote\": \"Contains only '\"\n" + + " \"DoubleQuote\": \"Contains only \\\"\"\n" + + " \"100\": \"Numeric key\"\n" + + " \"empty\": \"\"\n" + + " \"blank\": \" \"\n" + + " \"positiveInfinity\": .inf\n" + + " \"negativeInfinity\": -.inf\n" + + " \"NaN\": .nan\n", + new String(stream.toByteArray())); + } - try (JsonGenerator generator = createGenerator(version, stream)) { + @ParameterizedTest + @MethodSource(VERSIONS_SOURCE) + void testMappingOfValuesWithQuotesMinimized(String version) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + Map config = Map.ofEntries( + Map.entry(Yaml.Settings.YAML_VERSION, version), + Map.entry(Yaml.Settings.DUMP_MINIMIZE_QUOTES, true)); + writeMappingOfValues(config, null, stream); + assertEquals("values:\n" + + " BigDecimal: 3.14\n" + + " BigInteger: 1000000\n" + + " Boolean: true\n" + + " double: 2.71\n" + + " int: 2021\n" + + " long: 2022\n" + + " String: Just a String\n" + + " \"Null\": null\n" + + " Multiline: |-\n This line\n spans multiple\n lines\n" + + " MultilineQuotes: Contains both ' and \" (quote types)\n" + + " SingleQuote: Contains only '\n" + + " DoubleQuote: Contains only \"\n" + + " \"100\": Numeric key\n" + + " empty: \"\"\n" + + " blank: ' '\n" + + " positiveInfinity: .inf\n" + + " negativeInfinity: -.inf\n" + + " NaN: .nan\n", + new String(stream.toByteArray())); + } + + private void writeMappingOfValues(Map config, String version, OutputStream stream) { + try (JsonGenerator generator = config != null ? + createGenerator(config, stream) : + createGenerator(version, stream)) { generator.writeStartObject() .writeStartObject("values") .write("BigDecimal", new BigDecimal("3.14")) @@ -136,8 +268,8 @@ void testMappingOfValues(String version) { .writeNull("Null") .write("Multiline", "This line\nspans multiple\nlines") .write("MultilineQuotes", "Contains both ' and \" (quote types)") - .write("DoubleQuoted", "Contains only '") - .write("SingleQuoted", "Contains only \"") + .write("SingleQuote", "Contains only '") + .write("DoubleQuote", "Contains only \"") .write("100", "Numeric key") .write("empty", "") .write("blank", " ") @@ -147,27 +279,6 @@ void testMappingOfValues(String version) { .writeEnd() .writeEnd(); } - - assertEquals("values:\n" - + " BigDecimal: 3.14\n" - + " BigInteger: 1000000\n" - + " Boolean: true\n" - + " double: 2.71\n" - + " int: 2021\n" - + " long: 2022\n" - + " String: Just a String\n" - + " 'Null': null\n" - + " Multiline: |-\n This line\n spans multiple\n lines\n" - + " MultilineQuotes: |-\n Contains both ' and \" (quote types)\n" - + " DoubleQuoted: \"Contains only '\"\n" - + " SingleQuoted: 'Contains only \"'\n" - + " '100': Numeric key\n" - + " empty: ''\n" - + " blank: ' '\n" - + " positiveInfinity: .inf\n" - + " negativeInfinity: -.inf\n" - + " NaN: .nan\n", - new String(stream.toByteArray())); } @ParameterizedTest @@ -197,7 +308,7 @@ void testExplicitDocumentStart(String version) { writer.flush(); } - assertEquals("---\ntestKey: testValue\n", writer.toString()); + assertEquals("---\n\"testKey\": \"testValue\"\n", writer.toString()); } @Deprecated @@ -216,7 +327,7 @@ void testExplicitDocumentStartWithDeprecatedProperty(String version) { writer.flush(); } - assertEquals("---\ntestKey: testValue\n", writer.toString()); + assertEquals("---\n\"testKey\": \"testValue\"\n", writer.toString()); } @ParameterizedTest @@ -247,7 +358,7 @@ void testExplicitDocumentEnd(String version) { writer.flush(); } - assertEquals("testKey: testValue\n...\n", writer.toString()); + assertEquals("\"testKey\": \"testValue\"\n...\n", writer.toString()); } @Deprecated @@ -266,34 +377,76 @@ void testExplicitDocumentEndWithDeprecatedProperty(String version) { writer.flush(); } - assertEquals("testKey: testValue\n...\n", writer.toString()); + assertEquals("\"testKey\": \"testValue\"\n...\n", writer.toString()); } @ParameterizedTest @MethodSource(VERSIONS_SOURCE) void testSpecialStringsQuoted(String version) { StringWriter writer = new StringWriter(); + Map config = Map.ofEntries( + Map.entry(Yaml.Settings.YAML_VERSION, version), + Map.entry(Yaml.Settings.DUMP_MINIMIZE_QUOTES, true)); - try (JsonGenerator generator = createGenerator(version, writer)) { + try (JsonGenerator generator = createGenerator(config, writer)) { generator.writeStartObject() .write("#keywithhash", "value with: colon") .write("#anotherwithhash", "value with:colon but the :is not followed by a space") .write("key with spaces", "ends with colon:") - .write("key\twith\ttabs", "ends with hash #") + .write("key\twith\ttabs", "ends with hash (preceded by space) #") .write("hash# in the middle", "#hash at the start of the value") .write("hash\t# with tab", "value with hash (#) preceded by tab\t#") + .write(".inf", "Key is infinite") + .write(".NAN", "Key is not a number!") + .write("false", "Key is reserved word") + .write("array[]", "Key has indicators") + .writeEnd(); + + writer.flush(); + } + + assertEquals("" + + "\"#keywithhash\": \"value with: colon\"\n" + + "\"#anotherwithhash\": value with:colon but the :is not followed by a space\n" + + "key with spaces: \"ends with colon:\"\n" + // snakeyaml* adds quotes due to `\t` + + "\"key\\twith\\ttabs\": \"ends with hash (preceded by space) #\"\n" + + "hash# in the middle: \"#hash at the start of the value\"\n" + + "\"hash\\t# with tab\": \"value with hash (#) preceded by tab\\t#\"\n" + + "\".inf\": Key is infinite\n" + + "\".NAN\": Key is not a number!\n" + + "\"false\": Key is reserved word\n" + + "\"array[]\": Key has indicators\n", + writer.toString()); + } + + @ParameterizedTest + @MethodSource(VERSIONS_SOURCE) + void testNumericStringsUnquotedWithConfiguration(String version) { + StringWriter writer = new StringWriter(); + Map config = Map.ofEntries( + Map.entry(Yaml.Settings.YAML_VERSION, version), + Map.entry(Yaml.Settings.DUMP_MINIMIZE_QUOTES, true), + Map.entry(Yaml.Settings.DUMP_QUOTE_NUMERIC_STRINGS, false)); + + try (JsonGenerator generator = createGenerator(config, writer)) { + generator.writeStartObject() + .write(".inf", "Key is infinite") + .write("ValueIsInfinite", "-.INF") + .write(".NAN", "Key is not a number!") + .write("ValueIsNotNumber", ".NaN") + .write("ValueIsNumber", new BigDecimal("3.14").toString()) .writeEnd(); writer.flush(); } assertEquals("" - + "'#keywithhash': 'value with: colon'\n" - + "'#anotherwithhash': value with:colon but the :is not followed by a space\n" - + "key with spaces: 'ends with colon:'\n" - + "\"key\\twith\\ttabs\": 'ends with hash #'\n" - + "hash# in the middle: '#hash at the start of the value'\n" - + "\"hash\\t# with tab\": \"value with hash (#) preceded by tab\\t#\"\n", + + "\".inf\": Key is infinite\n" + + "ValueIsInfinite: -.INF\n" + + "\".NAN\": Key is not a number!\n" + + "ValueIsNotNumber: .NaN\n" + + "ValueIsNumber: 3.14\n", writer.toString()); } } diff --git a/src/test/java/io/xlate/yamljson/YamlTestHelper.java b/src/test/java/io/xlate/yamljson/YamlTestHelper.java index 4c8f8c6..78143d0 100644 --- a/src/test/java/io/xlate/yamljson/YamlTestHelper.java +++ b/src/test/java/io/xlate/yamljson/YamlTestHelper.java @@ -136,6 +136,10 @@ static JsonReader createReader(InputStream stream, Map properties) { ////////// + static JsonGenerator createGenerator(Map config, Writer writer) { + return Yaml.createGeneratorFactory(config).createGenerator(writer); + } + static JsonGenerator createGenerator(String version, Writer writer) { if (isOnlySupportedVersion(version)) { return Yaml.createGenerator(writer); @@ -152,6 +156,10 @@ static JsonGenerator createGenerator(String version, OutputStream stream) { return Yaml.createGeneratorFactory(Map.of(Yaml.Settings.YAML_VERSION, version)).createGenerator(stream); } + static JsonGenerator createGenerator(Map config, OutputStream stream) { + return Yaml.createGeneratorFactory(config).createGenerator(stream); + } + ////////// static JsonWriter createWriter(String version, Writer writer) { diff --git a/src/test/java/io/xlate/yamljson/YamlWriterTest.java b/src/test/java/io/xlate/yamljson/YamlWriterTest.java index 55587be..0a5899c 100644 --- a/src/test/java/io/xlate/yamljson/YamlWriterTest.java +++ b/src/test/java/io/xlate/yamljson/YamlWriterTest.java @@ -75,6 +75,6 @@ void testScalarStyles(String version) { assertThrows(IllegalStateException.class, () -> writer.write(value)); } - assertEquals("- '#http://example.com'\n", new String(stream.toByteArray(), StandardCharsets.UTF_8)); + assertEquals("- \"#http://example.com\"\n", new String(stream.toByteArray(), StandardCharsets.UTF_8)); } }