diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 5c4537da..c93c081b 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -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` diff --git a/release-notes/VERSION b/release-notes/VERSION index 949c9a3f..3240d611 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -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) diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java index 70261884..3dac6e64 100644 --- a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java @@ -1100,29 +1100,46 @@ public String getObjectId() throws JacksonException @Override public String getTypeId() throws JacksonException { - Optional 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). + *

+ * 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 !} 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 _currentTag() + { + if (_lastTagEvent instanceof CollectionStartEvent cse) { + return cse.getTag(); + } else if (_lastTagEvent instanceof ScalarEvent se) { + return se.getTag(); + } + return Optional.empty(); + } + /* /********************************************************************** /* Internal methods diff --git a/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java new file mode 100644 index 00000000..35138bbd --- /dev/null +++ b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java @@ -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 = "--- !\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()); + } + } +}