Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional options to allow control over style of generated YAML #138

Merged
merged 2 commits into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/main/java/io/xlate/yamljson/SnakeYamlEngineGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ class SnakeYamlEngineGenerator extends YamlGenerator<Event, ScalarStyle> impleme
final DumpSettings settings;
final Emitter emitter;

SnakeYamlEngineGenerator(DumpSettings settings, YamlWriterStream writer) {
super(STYLES, writer);
SnakeYamlEngineGenerator(Map<String, Object> 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<String, Object> properties, DumpSettings settings, Writer writer) {
this(properties, settings, new YamlWriterStream(writer));
}

@Override
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/xlate/yamljson/SnakeYamlGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ class SnakeYamlGenerator extends YamlGenerator<Event, ScalarStyle> implements Js
final DumperOptions settings;
final Emitter emitter;

SnakeYamlGenerator(DumperOptions settings, Writer writer) {
super(STYLES, writer);
SnakeYamlGenerator(Map<String, Object> properties, DumperOptions settings, Writer writer) {
super(properties, STYLES, writer);
this.settings = settings;
this.emitter = new Emitter(writer, settings);
}
Expand Down
95 changes: 71 additions & 24 deletions src/main/java/io/xlate/yamljson/StringQuotingChecker.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,40 @@
*/
class StringQuotingChecker {

StringQuotingChecker() {
private final boolean quoteNumericStrings;

StringQuotingChecker(boolean quoteNumericStrings) {
this.quoteNumericStrings = quoteNumericStrings;
}

/**
* Check whether given property name should be quoted: usually to prevent it
* from being read as non-String key (boolean or number)
*/
boolean needToQuoteName(String name) {
return isReservedKeyword(name) || YamlNumbers.isFloat(name);
return needToQuote(name, true);
}

/**
* Check whether given String value should be quoted: usually to prevent it
* 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 value.isEmpty() ||
isReservedKeyword(value) ||
hasQuoteableCharacter(value) ||
(quoteNumeric && YamlNumbers.isNumeric(value));
}

/**
* See if given String value is one of
* <ul>
* <li>YAML 1.2 keyword representing boolean</li>
* <li>YAML 1.2 keyword representing null value</li>
* <li>empty String (length 0)</li></li> and returns {@code true} if so.
*
* @param value
* String to check
Expand All @@ -40,10 +48,6 @@ boolean needToQuoteValue(String value) {
* (as per YAML 1.2 specification) or empty String
*/
boolean isReservedKeyword(String value) {
if (value.length() == 0) {
return true;
}

switch (value.charAt(0)) {
// First, reserved name starting chars:
case 'f': // false
Expand All @@ -62,23 +66,24 @@ boolean isReservedKeyword(String value) {
}

/**
* As per YAML <a href="https://yaml.org/spec/1.2/spec.html#id2788859">Plain
* As per YAML <a href="https://yaml.org/spec/1.2.2/#733-plain-style">Plain
* Style</a>unquoted strings are restricted to a reduced charset and must be
* quoted in case they contain one of the following characters or character
* combinations.
*/
boolean valueHasQuotableChar(String inputStr) {
boolean hasQuoteableCharacter(String inputStr) {
if (quotableLeadingCharacter(inputStr)) {
return true;
}

final int end = inputStr.length();
for (int i = 0; i < end; ++i) {
switch (inputStr.charAt(i)) {
case '[':
case ']':
case '{':
case '}':
case ',':
return true;

for (int i = 1; i < end; ++i) {
int current = inputStr.charAt(i);

switch (current) {
case '#':
if (precededByBlank(inputStr, i)) {
if (isBlank(inputStr.charAt(i - 1))) {
return true;
}
break;
Expand All @@ -88,17 +93,59 @@ boolean valueHasQuotableChar(String inputStr) {
}
break;
default:
if (current < 0x20) {
// Control character
return true;
}
break;
}
}
return false;

// Check for trailing space
return isBlank(inputStr.charAt(end - 1));
}

boolean precededByBlank(String inputStr, int offset) {
if (offset == 0) {
boolean quotableLeadingCharacter(String inputStr) {
final int first = inputStr.charAt(0);

switch (first) {
case ' ':
// Leading space
return true;
case '#':
case ',':
case '[':
case ']':
case '{':
case '}':
case '&':
case '*':
case '!':
case '|':
case '>':
case '"':
case '%':
case '@':
case '`':
// Leading indicators
return true;
case '?':
case ':':
case '-':
// Leading indicators not followed by non-space "safe" character
if (followedByBlank(inputStr, 0)) {
return true;
}
break;
default:
if (first < 0x20) {
// Control character
return true;
}
break;
}
return isBlank(inputStr.charAt(offset - 1));

return false;
}

boolean followedByBlank(String inputStr, int offset) {
Expand Down
52 changes: 49 additions & 3 deletions src/main/java/io/xlate/yamljson/Yaml.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ private Versions() {
*/
public static final class Settings {

private static final String PRE = "io.xlate.yamljson.";

private Settings() {
}

Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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).
* <p>
* Minimized quote usage makes for more human readable output; however,
* content is limited to printable characters according to the rules of
* <a href=
* "http://www.yaml.org/spec/1.2/spec.html#style/block/literal">literal
* block style</a>.
*
* @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 <a href=
* "http://www.yaml.org/spec/1.2/spec.html#style/block/literal">literal
* block style</a>. 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.
*<p>
* 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();
Expand Down
60 changes: 39 additions & 21 deletions src/main/java/io/xlate/yamljson/YamlGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> properties;
protected final Map<StyleType, S> styleTypes;
protected final Writer writer;
final Deque<ContextType> context = new ArrayDeque<>();

YamlGenerator(Map<StyleType, S> styleTypes, Writer writer) {
private final Deque<ContextType> 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<String, Object> properties, Map<StyleType, S> 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<String, Object> properties, String key, String defaultValue) {
Object value = properties.getOrDefault(key, defaultValue);
return Boolean.parseBoolean(String.valueOf(value));
}

protected abstract E getEvent(EventType type);
Expand Down Expand Up @@ -122,23 +140,22 @@ void emitScalar(Object value, boolean forcePlain, Predicate<String> 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);
}
}
}

Expand Down Expand Up @@ -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;
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/xlate/yamljson/YamlGeneratorFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/io/xlate/yamljson/YamlNumbers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading
Loading