From 56d5616938af5be943074222b9b10e0519b170b7 Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Fri, 18 Dec 2020 18:25:07 +0300 Subject: [PATCH] Serialize undefined as null in collections, remove it in mappings fix https://github.com/nodeca/js-yaml/issues/571 fix https://github.com/nodeca/js-yaml/issues/325 --- CHANGELOG.md | 3 + lib/dumper.js | 24 +++++--- test/issues/0571.js | 114 +++++++++++++++++++++++++++++++++++++ test/units/skip-invalid.js | 1 - 4 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 test/issues/0571.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 5df76ded..b42df10a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Line and column in exceptions are now formatted as `(X:Y)` instead of `at line X, column Y` (also present in compact format), #332. - Code snippet created in exceptions now contains multiple lines with line numbers. +- `dump()` now serializes `undefined` as `null` in collections and removes keys with + `undefined` in mappings, #571. +- `dump()` with `skipInvalid=true` now serializes invalid items in collections as null. ### Added - Added `.mjs` (es modules) support. diff --git a/lib/dumper.js b/lib/dumper.js index 66bf36e6..48730971 100644 --- a/lib/dumper.js +++ b/lib/dumper.js @@ -565,9 +565,12 @@ function writeFlowSequence(state, level, object) { length; for (index = 0, length = object.length; index < length; index += 1) { - // Write only valid elements. - if (writeNode(state, level, object[index], false, false)) { - if (index !== 0) _result += ',' + (!state.condenseFlow ? ' ' : ''); + // Write only valid elements, put null instead of invalid elements. + if (writeNode(state, level, object[index], false, false) || + (typeof object[index] === 'undefined' && + writeNode(state, level, null, false, false))) { + + if (_result !== '') _result += ',' + (!state.condenseFlow ? ' ' : ''); _result += state.dump; } } @@ -583,9 +586,12 @@ function writeBlockSequence(state, level, object, compact) { length; for (index = 0, length = object.length; index < length; index += 1) { - // Write only valid elements. - if (writeNode(state, level + 1, object[index], true, true, false, true)) { - if (!compact || index !== 0) { + // Write only valid elements, put null instead of invalid elements. + if (writeNode(state, level + 1, object[index], true, true, false, true) || + (typeof object[index] === 'undefined' && + writeNode(state, level + 1, null, true, true, false, true))) { + + if (!compact || _result !== '') { _result += generateNextLine(state, level); } @@ -616,7 +622,7 @@ function writeFlowMapping(state, level, object) { for (index = 0, length = objectKeyList.length; index < length; index += 1) { pairBuffer = ''; - if (index !== 0) pairBuffer += ', '; + if (_result !== '') pairBuffer += ', '; if (state.condenseFlow) pairBuffer += '"'; @@ -671,7 +677,7 @@ function writeBlockMapping(state, level, object, compact) { for (index = 0, length = objectKeyList.length; index < length; index += 1) { pairBuffer = ''; - if (!compact || index !== 0) { + if (!compact || _result !== '') { pairBuffer += generateNextLine(state, level); } @@ -823,6 +829,8 @@ function writeNode(state, level, object, block, compact, iskey, isblockseq) { if (state.tag !== '?') { writeScalar(state, state.dump, level, iskey, inblock); } + } else if (type === '[object Undefined]') { + return false; } else { if (state.skipInvalid) return false; throw new YAMLException('unacceptable kind of an object to dump ' + type); diff --git a/test/issues/0571.js b/test/issues/0571.js new file mode 100644 index 00000000..928d7eea --- /dev/null +++ b/test/issues/0571.js @@ -0,0 +1,114 @@ +'use strict'; + + +const assert = require('assert'); +const yaml = require('../../'); + + +describe('Undefined', function () { + let undef = new yaml.Type('!undefined', { + kind: 'scalar', + resolve: () => true, + construct: () => {}, + predicate: object => typeof object === 'undefined', + represent: () => '' + }); + + let undef_schema = yaml.DEFAULT_SCHEMA.extend(undef); + + + it('Should replace undefined with null in collections', function () { + let str; + + str = yaml.dump([ undefined, 1, undefined, null, 2 ], { flowLevel: 0 }); + assert(str.match(/^\[/)); + assert.deepStrictEqual( + yaml.load(str), + [ null, 1, null, null, 2 ] + ); + + str = yaml.dump([ undefined, 1, undefined, null, 2 ], { flowLevel: -1 }); + assert(str.match(/^- /)); + assert.deepStrictEqual( + yaml.load(str), + [ null, 1, null, null, 2 ] + ); + }); + + + it('Should remove keys with undefined in mappings', function () { + let str; + + str = yaml.dump({ t: undefined, foo: 1, bar: undefined, baz: null }, { flowLevel: 0 }); + assert(str.match(/^\{/)); + assert.deepStrictEqual( + yaml.load(str), + { foo: 1, baz: null } + ); + + str = yaml.dump({ t: undefined, foo: 1, bar: undefined, baz: null }, { flowLevel: -1 }); + assert(str.match(/^foo:/)); + assert.deepStrictEqual( + yaml.load(str), + { foo: 1, baz: null } + ); + }); + + + it("Should serialize top-level undefined to ''", function () { + assert.strictEqual(yaml.dump(undefined), ''); + }); + + + it('Should serialize undefined if schema is available', function () { + assert.deepStrictEqual( + yaml.load( + yaml.dump([ 1, undefined, null, 2 ], { schema: undef_schema }), + { schema: undef_schema } + ), + [ 1, undefined, null, 2 ] + ); + + assert.deepStrictEqual( + yaml.load( + yaml.dump({ foo: 1, bar: undefined, baz: null }, { schema: undef_schema }), + { schema: undef_schema } + ), + { foo: 1, bar: undefined, baz: null } + ); + }); + + + it('Should respect null formatting', function () { + assert.strictEqual( + yaml.dump([ undefined ], { styles: { '!!null': 'uppercase' } }), + '- NULL\n' + ); + }); + + + it('Should return an error if neither null nor undefined schemas are available', function () { + assert.throws(() => { + yaml.dump([ 'foo', undefined, 'bar' ], { schema: yaml.FAILSAFE_SCHEMA }); + }, /unacceptable kind of an object to dump/); + }); + + + it('Should skip leading values correctly', function () { + assert.strictEqual( + yaml.dump([ () => {}, 'a' ], { flowLevel: 0, skipInvalid: true }), + '[a]\n'); + + assert.strictEqual( + yaml.dump([ () => {}, 'a' ], { flowLevel: -1, skipInvalid: true }), + '- a\n'); + + assert.strictEqual( + yaml.dump({ a: () => {}, b: 'a' }, { flowLevel: 0, skipInvalid: true }), + '{b: a}\n'); + + assert.strictEqual( + yaml.dump({ a: () => {}, b: 'a' }, { flowLevel: -1, skipInvalid: true }), + 'b: a\n'); + }); +}); diff --git a/test/units/skip-invalid.js b/test/units/skip-invalid.js index f3d99e93..54e539f2 100644 --- a/test/units/skip-invalid.js +++ b/test/units/skip-invalid.js @@ -7,7 +7,6 @@ var yaml = require('../../'); var sample = { number: 42, - undef: undefined, string: 'hello', func: function (a, b) { return a + b; }, regexp: /^hel+o/,