diff --git a/packages/csv-stringify/lib/api/index.js b/packages/csv-stringify/lib/api/index.js index 392cf7fa..0f214501 100644 --- a/packages/csv-stringify/lib/api/index.js +++ b/packages/csv-stringify/lib/api/index.js @@ -121,6 +121,7 @@ const stringifier = function (options, state, info) { if (typeof value === "string") { options = this.options; } else if (is_object(value)) { + // Value is considerered as a mix of a value and options options = value; value = options.value; delete options.value; @@ -136,6 +137,7 @@ const stringifier = function (options, state, info) { ), ]; } + // Merge global options with the ones returned by cast options = { ...this.options, ...options }; [err, options] = normalize_options(options); if (err !== undefined) { @@ -303,8 +305,15 @@ const stringifier = function (options, state, info) { return [undefined, this.options.cast.date(value, context)]; } else if (type === "object" && value !== null) { return [undefined, this.options.cast.object(value, context)]; + } else if (value === null && this.options.cast.null !== undefined) { + return [undefined, this.options.cast.null(value, context)]; + } else if ( + value === undefined && + this.options.cast.undefined !== undefined + ) { + return [undefined, this.options.cast.undefined(value, context)]; } else { - return [undefined, value, value]; + return [undefined, value]; } } catch (err) { return [err]; diff --git a/packages/csv-stringify/lib/index.d.ts b/packages/csv-stringify/lib/index.d.ts index 76b53514..f93ba897 100644 --- a/packages/csv-stringify/lib/index.d.ts +++ b/packages/csv-stringify/lib/index.d.ts @@ -60,6 +60,8 @@ export interface OptionsNormalized extends stream.TransformOptions { */ object?: Cast>; string?: Cast; + null?: Cast; + undefined?: Cast; }; /** * List of fields, applied when `transform` returns an object, order matters, @@ -140,6 +142,8 @@ export interface Options extends stream.TransformOptions { */ object?: Cast>; string?: Cast; + null?: Cast; + undefined?: Cast; }; /** * List of fields, applied when `transform` returns an object, order matters, diff --git a/packages/csv-stringify/test/option.cast.ts b/packages/csv-stringify/test/option.cast.ts index f966eb77..3a17d49d 100644 --- a/packages/csv-stringify/test/option.cast.ts +++ b/packages/csv-stringify/test/option.cast.ts @@ -20,96 +20,6 @@ describe("Option `cast`", function () { }); describe("udf", function () { - it("handle string formatter", function (next) { - stringify( - [ - { - value: "ok", - }, - ], - { cast: { string: () => "X" } }, - (err, data) => { - if (!err) data.should.eql("X\n"); - next(err); - }, - ); - }); - - it("handle boolean formatter", function (next) { - stringify( - [ - { - value: true, - }, - ], - { cast: { boolean: () => "X" } }, - (err, data) => { - if (!err) data.should.eql("X\n"); - next(err); - }, - ); - }); - - it("handle date formatter", function (next) { - stringify( - [ - { - value: new Date(), - }, - ], - { cast: { date: () => "X" } }, - (err, data) => { - if (!err) data.should.eql("X\n"); - next(err); - }, - ); - }); - - it("handle number formatter", function (next) { - stringify( - [ - { - value: 3.14, - }, - ], - { cast: { number: (value) => "" + value * 2 } }, - (err, data) => { - if (!err) data.should.eql("6.28\n"); - next(err); - }, - ); - }); - - it("handle bigint formatter", function (next) { - stringify( - [ - { - value: BigInt(9007199254740991), - }, - ], - { cast: { bigint: (value) => "" + value / BigInt(2) } }, - (err, data) => { - if (!err) data.should.eql("4503599627370495\n"); - next(err); - }, - ); - }); - - it("handle object formatter", function (next) { - stringify( - [ - { - value: { a: 1 }, - }, - ], - { cast: { object: () => "X" } }, - (err, data) => { - if (!err) data.should.eql("X\n"); - next(err); - }, - ); - }); - it("catch error", function (next) { stringify( [ diff --git a/packages/csv-stringify/test/option.cast.types.ts b/packages/csv-stringify/test/option.cast.types.ts new file mode 100644 index 00000000..d87b71e3 --- /dev/null +++ b/packages/csv-stringify/test/option.cast.types.ts @@ -0,0 +1,189 @@ +import "should"; +import { stringify } from "../lib/index.js"; + +describe("Option `cast` with null and undefined", function () { + it("cast.bigint formatter", function (next) { + stringify( + [ + { + value: BigInt(9007199254740991), + }, + ], + { cast: { bigint: (value) => "" + value / BigInt(2) } }, + (err, data) => { + if (!err) data.should.eql("4503599627370495\n"); + next(err); + }, + ); + }); + + it("cast.boolean formatter", function (next) { + stringify( + [ + { + value: true, + }, + ], + { cast: { boolean: () => "X" } }, + (err, data) => { + if (!err) data.should.eql("X\n"); + next(err); + }, + ); + }); + + it("cast.date formatter", function (next) { + stringify( + [ + { + value: new Date(), + }, + ], + { cast: { date: () => "X" } }, + (err, data) => { + if (!err) data.should.eql("X\n"); + next(err); + }, + ); + }); + + it("cast.null can emit unquoted NULL", function (next) { + stringify( + [{ a: "foo", b: null, c: "bar" }], + { + header: true, + cast: { + null: function () { + return { value: "NULL", quoted: false }; + }, + }, + }, + (err, data) => { + if (!err) { + data.should.eql("a,b,c\nfoo,NULL,bar\n"); + } + next(err); + }, + ); + }); + + it("cast.null receives expected context", function (next) { + stringify( + [[null]], + { + cast: { + null: function (value, context) { + Object.keys(context) + .sort() + .should.eql(["column", "header", "index", "records"]); + return ""; + }, + }, + }, + next, + ); + }); + + it("cast.null returning a string respects quoting rules", function (next) { + stringify( + [{ a: "foo", b: null, c: "bar" }], + { + quoted: true, + cast: { + null: function () { + return "NULL"; + }, + }, + }, + (err, data) => { + if (!err) { + data.should.eql('"foo","NULL","bar"\n'); + } + next(err); + }, + ); + }); + + it("cast.null catches error and surfaces it", function (next) { + stringify( + [[null]], + { + cast: { + null: function () { + throw Error("Catchme"); + }, + }, + }, + (err) => { + if (!err) return next(Error("Invalid assessment")); + err.message.should.eql("Catchme"); + next(); + }, + ); + }); + + it("cast.number formatter", function (next) { + stringify( + [ + { + value: 3.14, + }, + ], + { cast: { number: (value) => "" + value * 2 } }, + (err, data) => { + if (!err) data.should.eql("6.28\n"); + next(err); + }, + ); + }); + + it("cast.object formatter", function (next) { + stringify( + [ + { + value: { a: 1 }, + }, + ], + { cast: { object: () => "X" } }, + (err, data) => { + if (!err) data.should.eql("X\n"); + next(err); + }, + ); + }); + + it("cast.string formatter", function (next) { + stringify( + [ + { + value: "ok", + }, + ], + { cast: { string: () => "X" } }, + (err, data) => { + if (!err) data.should.eql("X\n"); + next(err); + }, + ); + }); + + it("cast.undefined can emit unquoted NULL", function (next) { + stringify( + [{ a: "foo", b: undefined, c: "bar" }], + { + header: true, + cast: { + undefined: function () { + return { value: "NULL", quoted: false }; + }, + }, + }, + (err, data) => { + if (!err) { + data.should.eql("a,b,c\nfoo,NULL,bar\n"); + } + next(err); + }, + ); + }); +});