Skip to content

Add FlowStyleResolver to enable custom YAML node style resolution #242

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

Closed
wants to merge 2 commits into from
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.net.URL;
import java.nio.charset.Charset;

import com.fasterxml.jackson.dataformat.yaml.util.NodeStyleResolver;
import org.yaml.snakeyaml.DumperOptions;

import com.fasterxml.jackson.core.*;
Expand Down Expand Up @@ -66,6 +67,13 @@ public class YAMLFactory extends JsonFactory
*/
protected final StringQuotingChecker _quotingChecker;

/**
* Helper object used to determine node styles of objects and arrays.
*
* @since 2.13
*/
protected final NodeStyleResolver _nodeStyleResolver;

/*
/**********************************************************************
/* Factory construction, configuration
Expand Down Expand Up @@ -94,6 +102,7 @@ public YAMLFactory(ObjectCodec oc)
//_version = DumperOptions.Version.V1_1;
_version = null;
_quotingChecker = StringQuotingChecker.Default.instance();
_nodeStyleResolver = NodeStyleResolver.DEFAULT_INSTANCE;
}

/**
Expand All @@ -106,6 +115,7 @@ public YAMLFactory(YAMLFactory src, ObjectCodec oc)
_yamlGeneratorFeatures = src._yamlGeneratorFeatures;
_version = src._version;
_quotingChecker = src._quotingChecker;
_nodeStyleResolver = src._nodeStyleResolver;
}

/**
Expand All @@ -119,6 +129,7 @@ protected YAMLFactory(YAMLFactoryBuilder b)
_yamlGeneratorFeatures = b.formatGeneratorFeaturesMask();
_version = b.yamlVersionToWrite();
_quotingChecker = b.stringQuotingChecker();
_nodeStyleResolver = b.nodeStyleResolver();
}

@Override
Expand Down Expand Up @@ -490,7 +501,7 @@ protected YAMLParser _createParser(byte[] data, int offset, int len, IOContext c
protected YAMLGenerator _createGenerator(Writer out, IOContext ctxt) throws IOException {
int feats = _yamlGeneratorFeatures;
YAMLGenerator gen = new YAMLGenerator(ctxt, _generatorFeatures, feats,
_quotingChecker, _objectCodec, out, _version);
_quotingChecker, _nodeStyleResolver, _objectCodec, out, _version);
// any other initializations? No?
return gen;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.fasterxml.jackson.dataformat.yaml;

import com.fasterxml.jackson.dataformat.yaml.util.NodeStyleResolver;
import org.yaml.snakeyaml.DumperOptions;

import com.fasterxml.jackson.core.TSFBuilder;
Expand Down Expand Up @@ -33,6 +34,13 @@ public class YAMLFactoryBuilder extends TSFBuilder<YAMLFactory, YAMLFactoryBuild
*/
protected StringQuotingChecker _quotingChecker;

/**
* Helper object used to determine node styles of objects and arrays.
*
* @since 2.13
*/
protected NodeStyleResolver _nodeStyleResolver;

/**
* YAML version for underlying generator to follow, if specified;
* left as {@code null} for backwards compatibility (which means
Expand All @@ -55,6 +63,7 @@ public YAMLFactoryBuilder(YAMLFactory base) {
_formatGeneratorFeatures = base._yamlGeneratorFeatures;
_version = base._version;
_quotingChecker = base._quotingChecker;
_nodeStyleResolver = base._nodeStyleResolver;
}

// // // Parser features NOT YET defined
Expand Down Expand Up @@ -111,12 +120,28 @@ public YAMLFactoryBuilder configure(YAMLGenerator.Feature f, boolean state) {
* default one (see {@code StringQuotingChecker.Default.instance()})
*
* @return This builder instance, to allow chaining
*
* @since 2.12
*/
public YAMLFactoryBuilder stringQuotingChecker(StringQuotingChecker sqc) {
_quotingChecker = sqc;
return this;
}

/**
* Method to specify a custom {@link NodeStyleResolver} to specify custom
* {@link com.fasterxml.jackson.dataformat.yaml.util.NodeStyleResolver.NodeStyle}s
* while serializing YAML objects and arrays.
*
* @return This builder instance, to allow chaining
*
* @since 2.13
*/
public YAMLFactoryBuilder nodeStyleResolver(NodeStyleResolver nodeStyleResolver) {
_nodeStyleResolver = nodeStyleResolver;
return this;
}

/**
* Method for specifying YAML version for generator to use (to produce
* compliant output); if {@code null} passed, will let {@code SnakeYAML}
Expand Down Expand Up @@ -145,13 +170,23 @@ public DumperOptions.Version yamlVersionToWrite() {
return _version;
}

/**
* @since 2.12
*/
public StringQuotingChecker stringQuotingChecker() {
if (_quotingChecker != null) {
return _quotingChecker;
}
return StringQuotingChecker.Default.instance();
}

/**
* @since 2.13
*/
public NodeStyleResolver nodeStyleResolver() {
return _nodeStyleResolver;
}

@Override
public YAMLFactory build() {
return new YAMLFactory(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.util.Map;
import java.util.regex.Pattern;

import com.fasterxml.jackson.dataformat.yaml.util.NodeStyleResolver;
import com.fasterxml.jackson.dataformat.yaml.util.NodeStyleResolver.NodeStyle;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.DumperOptions.FlowStyle;
import org.yaml.snakeyaml.emitter.Emitter;
Expand Down Expand Up @@ -241,6 +243,8 @@ private Feature(boolean defaultState) {

protected final StringQuotingChecker _quotingChecker;

protected final NodeStyleResolver _nodeStyleResolver;

/*
/**********************************************************
/* Life-cycle
Expand All @@ -249,6 +253,7 @@ private Feature(boolean defaultState) {

public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures,
StringQuotingChecker quotingChecker,
NodeStyleResolver nodeStyleResolver,
ObjectCodec codec, Writer out,
org.yaml.snakeyaml.DumperOptions.Version version)
throws IOException
Expand All @@ -258,6 +263,8 @@ public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures,
_formatFeatures = yamlFeatures;
_quotingChecker = (quotingChecker == null)
? StringQuotingChecker.Default.instance() : quotingChecker;
_nodeStyleResolver = (nodeStyleResolver == null)
? NodeStyleResolver.DEFAULT_INSTANCE : nodeStyleResolver;
_writer = out;
_docVersion = version;

Expand All @@ -273,7 +280,7 @@ public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures,
public YAMLGenerator(IOContext ctxt, int jsonFeatures, int yamlFeatures,
ObjectCodec codec, Writer out,
org.yaml.snakeyaml.DumperOptions.Version version) throws IOException {
this(ctxt, jsonFeatures, yamlFeatures, null,
this(ctxt, jsonFeatures, yamlFeatures, null, null,
codec, out, version);
}

Expand Down Expand Up @@ -517,13 +524,16 @@ public final void writeStartArray() throws IOException
{
_verifyValueWrite("start an array");
_writeContext = _writeContext.createChildArrayContext();
FlowStyle style = _outputOptions.getDefaultFlowStyle();
String yamlTag = _typeId;
boolean implicit = (yamlTag == null);
String anchor = _objectId;
if (anchor != null) {
_objectId = null;
}
NodeStyle jacksonStyle = _nodeStyleResolver.resolveStyle(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would make sense to use getCurrentName() bit earlier here, before createChildArrayContext().

Would this work with nested arrays? (might be that it gets null).

Also: would make sense to get and pass default style to custom resolver -- but resolver could be passed the context as well (pre-addition of array), to let it access all information, including ancestors. I'll add a note on interface later on.

_writeContext.getParent().getCurrentName());
FlowStyle style = jacksonStyle != null ? jacksonStyle.getSnakeYamlFlowStyle()
: _outputOptions.getDefaultFlowStyle();
_emit(new SequenceStartEvent(anchor, yamlTag,
implicit, null, null, style));
}
Expand All @@ -545,13 +555,16 @@ public final void writeStartObject() throws IOException
{
_verifyValueWrite("start an object");
_writeContext = _writeContext.createChildObjectContext();
FlowStyle style = _outputOptions.getDefaultFlowStyle();
String yamlTag = _typeId;
boolean implicit = (yamlTag == null);
String anchor = _objectId;
if (anchor != null) {
_objectId = null;
}
NodeStyle jacksonStyle = _nodeStyleResolver.resolveStyle(
_writeContext.getParent().getCurrentName());
FlowStyle style = jacksonStyle != null ? jacksonStyle.getSnakeYamlFlowStyle()
: _outputOptions.getDefaultFlowStyle();
_emit(new MappingStartEvent(anchor, yamlTag,
implicit, null, null, style));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package com.fasterxml.jackson.dataformat.yaml.util;

import org.yaml.snakeyaml.DumperOptions;

/**
* Helper interface to customize node styles of object and arrays while exporting YAML objects.
*
* @see NodeStyle
* @see #resolveStyle(String)
*/
public interface NodeStyleResolver {

/**
* Defines which style to apply to a given object or array.
*
* @see DumperOptions.FlowStyle
* @see <a href="http://www.yaml.org/spec/current.html#id2509255">3.2.3.1.
* Node Styles (http://yaml.org/spec/1.1)</a>
*/
enum NodeStyle {
/**
* Block style. i.e.
* <pre>
* foo:
* - bar
* </pre>
* <pre>
* key:
* foo: bar
* </pre>
*/
BLOCK,
/**
* Flow style. i.e.
* <pre>
* foo: [bar]
* </pre>
* <pre>
* key: {foo: bar}
* </pre>
*/
FLOW;

public DumperOptions.FlowStyle getSnakeYamlFlowStyle() {
switch (this) {
case BLOCK:
return DumperOptions.FlowStyle.BLOCK;
case FLOW:
return DumperOptions.FlowStyle.FLOW;
default:
throw new IllegalStateException("Unexpected value: " + this);
}
}
}

NodeStyleResolver DEFAULT_INSTANCE = new NodeStyleResolver() {
@Override
public NodeStyle resolveStyle(String fieldName) {
// default behaviour uses YAMLGenerator._outputOptions.getDefaultFlowStyle() (currently set to BLOCK)
return null;
}
};

/**
* Resolve a node style for given fieldName.
*
* @param fieldName parent field name of the current object or array. can be null if there is no parent field (i.e.
* typically root object)
* @return the desired {@link NodeStyle} or null to use default value (currently 'BLOCK')
*/
NodeStyle resolveStyle(String fieldName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that instead of passing just fieldName, it'd be more flexible to pass write context; and perhaps also have 2 methods to call: one for Objects, another for Arrays.
I guess it would be possible to pass both name (current name in context) and context, to simplify some cases, while giving full power for others.

At very least I think array and object cases should go to different methods since I think that's relatively common use case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... just realized I added this note already. Wrt comment I just added above.

And looking at this method now, maybe passing the default style is not useful after all (because it would need to be translated etc). Returning null makes sense.


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.fasterxml.jackson.dataformat.yaml.ser;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.ModuleTestBase;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import com.fasterxml.jackson.dataformat.yaml.util.NodeStyleResolver;

import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;

public class CustomNodeStyleTest extends ModuleTestBase {
static class CustomNodeStyleResolver implements NodeStyleResolver {
@Override
public NodeStyle resolveStyle(String fieldName) {
if (fieldName != null && fieldName.endsWith("_flow"))
return NodeStyle.FLOW;
else if (fieldName != null && fieldName.endsWith("_block"))
return NodeStyle.BLOCK;
else
return null;
}
}

private final ObjectMapper REGULAR_MAPPER = YAMLMapper.builder()
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.build();

private final YAMLMapper CUSTOM_MAPPER = YAMLMapper.builder(
YAMLFactory.builder()
.nodeStyleResolver(new CustomNodeStyleResolver())
.build())
.enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.build();

public void testFlowStyles() throws Exception {
// list
assertEquals("key_flow: [value]",
_asYaml(CUSTOM_MAPPER, singletonMap("key_flow", singletonList("value"))));
assertEquals("key_block:\n- value",
_asYaml(CUSTOM_MAPPER, singletonMap("key_block", singletonList("value"))));
assertEquals("key_default:\n- value",
_asYaml(REGULAR_MAPPER, singletonMap("key_default", singletonList("value"))));

// object
assertEquals("key_flow: {foo: bar}",
_asYaml(CUSTOM_MAPPER, singletonMap("key_flow", singletonMap("foo", "bar"))));
assertEquals("key_block:\n foo: bar",
_asYaml(CUSTOM_MAPPER, singletonMap("key_block", singletonMap("foo", "bar"))));
assertEquals("key_default:\n foo: bar",
_asYaml(REGULAR_MAPPER, singletonMap("key_default", singletonMap("foo", "bar"))));
}

private String _asYaml(ObjectMapper mapper, Object value) throws Exception {
return mapper.writeValueAsString(value).trim();
}
}