Skip to content
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
4 changes: 4 additions & 0 deletions release-notes/CREDITS
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Martin Winandy (@pmwmedia)
* Reported #109: (yaml) `YAMLMapper` doesn't close output file if an error occurs
(3.2.0)

Alexander Tsvetkov (@nictas)
* Requested #214: (yaml) Expose custom tags via `YAMLParser.getRawTag()`
(3.2.0)

David Xia (@davidxia)
* Requested #276: (yaml) Support opting out of octal number interpretation (like "0444");
add `YAMLReadFeature.PARSE_OCTAL_NUMBERS`
Expand Down
3 changes: 3 additions & 0 deletions release-notes/VERSION
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ implementations)
#109: (yaml) `YAMLMapper` doesn't close output file if an error occurs
(reported by Martin W)
(fix by @cowtowncoder, w/ Claude code)
#214: (yaml) Expose custom tags via `YAMLParser.getRawTag()`
(requested by Alexander T)
(fix by @cowtowncoder, w/ Claude code)
#264: (csv) `CsvMapper` applies alphabetic Property Order for Java Records
(reported by @alexpartsch)
(fix by @cowtowncoder)
Expand Down
49 changes: 33 additions & 16 deletions yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -1100,29 +1100,46 @@ public String getObjectId() throws JacksonException
@Override
public String getTypeId() throws JacksonException
{
Optional<String> tagOpt;
if (_lastTagEvent instanceof CollectionStartEvent) {
tagOpt = ((CollectionStartEvent) _lastTagEvent).getTag();
//System.err.println("getTypeId() at "+currentToken()+", last was collection ("+_lastTagEvent.getClass().getSimpleName()+") -> "+tag);
} else if (_lastTagEvent instanceof ScalarEvent) {
tagOpt = ((ScalarEvent) _lastTagEvent).getTag();
//System.err.println("getTypeId() at "+currentToken()+", last was scalar -> "+tag+", scalar == "+_lastEvent);
} else {
//System.err.println("getTypeId(), something else, curr token: "+currentToken());
return null;
}
if (tagOpt.isPresent()) {
String tag = tagOpt.get();
String tag = _currentTag().orElse(null);
if (tag != null) {
// 04-Aug-2013, tatu: Looks like YAML parser's expose these in... somewhat exotic
// ways sometimes. So let's prepare to peel off some wrappings:
while (tag.startsWith("!")) {
tag = tag.substring(1);
}
return tag;
}
return null;
return tag;
}


/**
* Method that can be used to access the YAML tag of the current token,
* in its raw form: that is, without stripping leading "!" prefix(es)
* (unlike {@link #getTypeId()} which does strip them).
*<p>
* Note that "raw" here means raw with respect to Jackson's processing;
* SnakeYAML Engine may have already resolved some tag syntax (for example,
* verbatim tags like {@code !<tag>} are resolved to their URI content).
*
* @return Raw YAML tag of the current token, if any; {@code null} if none
*
* @since 3.2
*/
public String getRawTag()
{
return _currentTag().orElse(null);
}

// @since 3.2
protected Optional<String> _currentTag()
{
if (_lastTagEvent instanceof CollectionStartEvent cse) {
return cse.getTag();
} else if (_lastTagEvent instanceof ScalarEvent se) {
return se.getTag();
}
return Optional.empty();
}

/*
/**********************************************************************
/* Internal methods
Expand Down
159 changes: 159 additions & 0 deletions yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package tools.jackson.dataformat.yaml.type;

import org.junit.jupiter.api.Test;

import tools.jackson.core.JsonToken;
import tools.jackson.dataformat.yaml.ModuleTestBase;
import tools.jackson.dataformat.yaml.YAMLMapper;
import tools.jackson.dataformat.yaml.YAMLParser;

import static org.junit.jupiter.api.Assertions.*;

/**
* Tests for {@link YAMLParser#getRawTag()}.
*
* @since 3.2
*/
public class RawTagTest extends ModuleTestBase
{
private final YAMLMapper MAPPER = new YAMLMapper();

@Test
public void testCustomScalarTag() throws Exception
{
final String YAML = "---\npassword: !sensitive Abcd1234\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertNull(p.getRawTag());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("password", p.currentName());

assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("Abcd1234", p.getText());
assertEquals("!sensitive", p.getRawTag());
// getTypeId() should strip the "!" prefix
assertEquals("sensitive", p.getTypeId());

assertToken(JsonToken.END_OBJECT, p.nextToken());
assertNull(p.getRawTag());
}
}

@Test
public void testVerbatimTag() throws Exception
{
// Verbatim tag !<...> is resolved by SnakeYAML Engine into the URI content
final String YAML = "--- !<impl>\na: 13\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertEquals("impl", p.getRawTag());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(13, p.getIntValue());

assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}

@Test
public void testLocalTag() throws Exception
{
// Local tag !impl keeps the "!" prefix
final String YAML = "--- !impl\na: 13\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertEquals("!impl", p.getRawTag());
// getTypeId() strips the "!" prefix
assertEquals("impl", p.getTypeId());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
assertEquals(13, p.getIntValue());

assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}

@Test
public void testNoTag() throws Exception
{
final String YAML = "---\nkey: value\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());
assertNull(p.getRawTag());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("value", p.getText());
assertNull(p.getRawTag());

assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}

@Test
public void testSequenceTag() throws Exception
{
final String YAML = "--- !mylist\n- a\n- b\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_ARRAY, p.nextToken());
assertEquals("!mylist", p.getRawTag());

assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("a", p.getText());
assertNull(p.getRawTag());

assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("b", p.getText());
assertNull(p.getRawTag());

assertToken(JsonToken.END_ARRAY, p.nextToken());
}
}

@Test
public void testSecondaryTagHandle() throws Exception
{
// "!!" is the secondary tag handle, resolved by SnakeYAML Engine
// to the "tag:yaml.org,2002:" prefix
final String YAML = "---\nvalue: !!str 123\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("123", p.getText());
// Raw tag includes the resolved "tag:yaml.org,2002:" prefix
assertEquals("tag:yaml.org,2002:str", p.getRawTag());
// getTypeId() strips "!" but this tag has none, so same value
assertEquals("tag:yaml.org,2002:str", p.getTypeId());

assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}

@Test
public void testMultipleCustomTags() throws Exception
{
final String YAML = "---\nuser: !public someone\npass: !sensitive Abcd1234\n";
try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) {
assertToken(JsonToken.START_OBJECT, p.nextToken());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("user", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("someone", p.getText());
assertEquals("!public", p.getRawTag());

assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
assertEquals("pass", p.currentName());
assertToken(JsonToken.VALUE_STRING, p.nextToken());
assertEquals("Abcd1234", p.getText());
assertEquals("!sensitive", p.getRawTag());

assertToken(JsonToken.END_OBJECT, p.nextToken());
}
}
}