diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ea8c9dc..2e50fa09 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `styles: { '!!null': 'empty' }` option for dumper (serializes `{ foo: null }` as "`foo: `"), #570. - Added `replacer` option (similar to option in JSON.stringify), #339. +- Custom `Tag` can now handle all tags or multiple tags with the same prefix, #385. ### Fixed - Astral characters are no longer encoded by dump/safeDump, #587. diff --git a/examples/handle_unknown_types.js b/examples/handle_unknown_types.js new file mode 100644 index 00000000..262711b3 --- /dev/null +++ b/examples/handle_unknown_types.js @@ -0,0 +1,42 @@ +'use strict'; + +/*eslint-disable no-console*/ + +var fs = require('fs'); +var path = require('path'); +var util = require('util'); +var yaml = require('../'); + + +var tags = [ 'scalar', 'sequence', 'mapping' ].map(function (kind) { + // first argument here is a prefix, so this type will handle anything starting with ! + return new yaml.Type('!', { + kind: kind, + multi: true, + construct: function (data, type) { + return { type: type, data: data }; + } + }); +}); + +var SCHEMA = yaml.DEFAULT_SCHEMA.extend(tags); + +// do not execute the following if file is required (http://stackoverflow.com/a/6398335) +if (require.main === module) { + + // And read a document using that schema. + fs.readFile(path.join(__dirname, 'handle_unknown_types.yml'), 'utf8', function (error, data) { + var loaded; + + if (!error) { + loaded = yaml.load(data, { schema: SCHEMA }); + console.log(util.inspect(loaded, false, 20, true)); + } else { + console.error(error.stack || error.message || String(error)); + } + }); +} + +// There are some exports to play with this example interactively. +module.exports.tags = tags; +module.exports.SCHEMA = SCHEMA; diff --git a/examples/handle_unknown_types.yml b/examples/handle_unknown_types.yml new file mode 100644 index 00000000..03fef24b --- /dev/null +++ b/examples/handle_unknown_types.yml @@ -0,0 +1,4 @@ +subject: Handling unknown types in JS-YAML +scalar: !unknown_scalar_tag 123 +sequence: !unknown_sequence_tag [ 1, 2, 3 ] +mapping: !unknown_mapping_tag { foo: 1, bar: 2 } diff --git a/lib/loader.js b/lib/loader.js index 2ee4bbe6..c5fc5a47 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -1334,6 +1334,7 @@ function composeNode(state, parentIndent, nodeContext, allowToSeek, allowCompact hasContent = false, typeIndex, typeQuantity, + typeList, type, flowIndent, blockIndent; @@ -1459,23 +1460,38 @@ function composeNode(state, parentIndent, nodeContext, allowToSeek, allowCompact break; } } - } else if (_hasOwnProperty.call(state.typeMap[state.kind || 'fallback'], state.tag)) { - type = state.typeMap[state.kind || 'fallback'][state.tag]; + } else { + if (_hasOwnProperty.call(state.typeMap[state.kind || 'fallback'], state.tag)) { + type = state.typeMap[state.kind || 'fallback'][state.tag]; + } else { + // looking for multi type + type = null; + typeList = state.typeMap.multi[state.kind || 'fallback']; + + for (typeIndex = 0, typeQuantity = typeList.length; typeIndex < typeQuantity; typeIndex += 1) { + if (state.tag.slice(0, typeList[typeIndex].tag.length) === typeList[typeIndex].tag) { + type = typeList[typeIndex]; + break; + } + } + } + + if (!type) { + throwError(state, 'unknown tag !<' + state.tag + '>'); + } if (state.result !== null && type.kind !== state.kind) { throwError(state, 'unacceptable node kind for !<' + state.tag + '> tag; it should be "' + type.kind + '", not "' + state.kind + '"'); } - if (!type.resolve(state.result)) { // `state.result` updated in resolver if matched + if (!type.resolve(state.result, state.tag)) { // `state.result` updated in resolver if matched throwError(state, 'cannot resolve a node with !<' + state.tag + '> explicit tag'); } else { - state.result = type.construct(state.result); + state.result = type.construct(state.result, state.tag); if (state.anchor !== null) { state.anchorMap[state.anchor] = state.result; } } - } else { - throwError(state, 'unknown tag !<' + state.tag + '>'); } } diff --git a/lib/schema.js b/lib/schema.js index ebfe4499..ae193562 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -11,7 +11,10 @@ function compileList(schema, name, result) { schema[name].forEach(function (currentType) { result.forEach(function (previousType, previousIndex) { - if (previousType.tag === currentType.tag && previousType.kind === currentType.kind) { + if (previousType.tag === currentType.tag && + previousType.kind === currentType.kind && + previousType.multi === currentType.multi) { + exclude.push(previousIndex); } }); @@ -30,11 +33,22 @@ function compileMap(/* lists... */) { scalar: {}, sequence: {}, mapping: {}, - fallback: {} + fallback: {}, + multi: { + scalar: [], + sequence: [], + mapping: [], + fallback: [] + } }, index, length; function collectType(type) { - result[type.kind][type.tag] = result['fallback'][type.tag] = type; + if (type.multi) { + result.multi[type.kind].push(type); + result.multi['fallback'].push(type); + } else { + result[type.kind][type.tag] = result['fallback'][type.tag] = type; + } } for (index = 0, length = arguments.length; index < length; index += 1) { @@ -79,6 +93,10 @@ Schema.prototype.extend = function extend(definition) { if (type.loadKind && type.loadKind !== 'scalar') { throw new YAMLException('There is a non-scalar type in the implicit list of a schema. Implicit resolving of such types is not supported.'); } + + if (type.multi) { + throw new YAMLException('There is a multi type in the implicit list of a schema. Multi tags can only be listed as explicit.'); + } }); explicit.forEach(function (type) { diff --git a/lib/type.js b/lib/type.js index 90b702ac..242479f8 100644 --- a/lib/type.js +++ b/lib/type.js @@ -4,6 +4,7 @@ var YAMLException = require('./exception'); var TYPE_CONSTRUCTOR_OPTIONS = [ 'kind', + 'multi', 'resolve', 'construct', 'instanceOf', @@ -51,6 +52,7 @@ function Type(tag, options) { this.predicate = options['predicate'] || null; this.represent = options['represent'] || null; this.defaultStyle = options['defaultStyle'] || null; + this.multi = options['multi'] || false; this.styleAliases = compileStyleAliases(options['styleAliases'] || null); if (YAML_NODE_KINDS.indexOf(this.kind) === -1) { diff --git a/test/issues/0385.js b/test/issues/0385.js new file mode 100644 index 00000000..1fc9fa02 --- /dev/null +++ b/test/issues/0385.js @@ -0,0 +1,99 @@ +'use strict'; + + +const assert = require('assert'); +const yaml = require('../../'); + + +describe('Multi tag', function () { + it('should process multi tags', function () { + let tags = [ 'scalar', 'mapping', 'sequence' ].map(kind => + new yaml.Type('!', { + kind, + multi: true, + resolve: function () { + return true; + }, + construct: function (value, tag) { + return { kind, tag, value }; + } + }) + ); + + let schema = yaml.DEFAULT_SCHEMA.extend(tags); + + let expected = [ + { + kind: 'scalar', + tag: '!t1', + value: '123' + }, + { + kind: 'sequence', + tag: '!t2', + value: [ 1, 2, 3 ] + }, + { + kind: 'mapping', + tag: '!t3', + value: { a: 1, b: 2 } + } + ]; + + assert.deepStrictEqual(yaml.load(` +- !t1 123 +- !t2 [ 1, 2, 3 ] +- !t3 { a: 1, b: 2 } +`, { + schema: schema + }), expected); + }); + + + it('should process tags depending on prefix', function () { + let tags = [ '!foo', '!bar', '!' ].map(prefix => + new yaml.Type(prefix, { + kind: 'scalar', + multi: true, + resolve: function () { + return true; + }, + construct: function (value, tag) { + return { prefix, tag, value }; + } + }) + ); + + tags.push( + new yaml.Type('!bar', { + kind: 'scalar', + resolve: function () { + return true; + }, + construct: function (value) { + return { single: true, value }; + } + }) + ); + + let schema = yaml.DEFAULT_SCHEMA.extend(tags); + + let expected = [ + { prefix: '!foo', tag: '!foo', value: '1' }, + { prefix: '!foo', tag: '!foo2', value: '2' }, + { single: true, value: '3' }, + { prefix: '!bar', tag: '!bar2', value: '4' }, + { prefix: '!', tag: '!baz', value: '5' } + ]; + + assert.deepStrictEqual(yaml.load(` +- !foo 1 +- !foo2 2 +- !bar 3 +- !bar2 4 +- !baz 5 +`, { + schema: schema + }), expected); + }); +});