Skip to content

Commit dca626c

Browse files
authored
feat(yaml/unstable): allow to add custom types for parse and stringify (#6841)
1 parent 2c356fc commit dca626c

File tree

7 files changed

+227
-6
lines changed

7 files changed

+227
-6
lines changed

yaml/_schema.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,12 @@ import { undefinedType } from "./_type/undefined.ts";
4646
*/
4747
export type SchemaType = "failsafe" | "json" | "core" | "default" | "extended";
4848

49-
type ImplicitType = Type<"scalar">;
50-
type ExplicitType = Type<KindType>;
49+
/**
50+
* A type that can be implicitly resolved (i.e. without using a tag) when
51+
* loading a YAML document.
52+
*/
53+
export type ImplicitType = Type<"scalar">;
54+
export type ExplicitType = Type<KindType>;
5155

5256
export type TypeMap = Record<
5357
KindType | "fallback",
@@ -161,3 +165,19 @@ export const SCHEMA_MAP = new Map<SchemaType, Schema>([
161165
["json", JSON_SCHEMA],
162166
["extended", EXTENDED_SCHEMA],
163167
]);
168+
169+
export function getSchema(
170+
schema: SchemaType = "default",
171+
types?: ImplicitType[],
172+
): Schema {
173+
const schemaObj = SCHEMA_MAP.get(schema)!;
174+
175+
if (!types) {
176+
return schemaObj;
177+
}
178+
179+
return createSchema({
180+
implicitTypes: [...types, ...schemaObj.implicitTypes],
181+
explicitTypes: [...schemaObj.explicitTypes],
182+
});
183+
}

yaml/_type.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
// Copyright 2018-2025 the Deno authors. MIT license.
55
// This module is browser compatible.
66

7+
/**
8+
* The kind of YAML node.
9+
*/
710
export type KindType = "sequence" | "scalar" | "mapping";
811
/**
912
* The style variation for `styles` option of {@linkcode stringify}
@@ -17,17 +20,30 @@ export type StyleVariant =
1720
| "octal"
1821
| "hexadecimal";
1922

23+
/**
24+
* Function to convert data to a string for YAML serialization.
25+
*/
2026
export type RepresentFn<D> = (data: D, style?: StyleVariant) => string;
2127

28+
/**
29+
* A type definition for a YAML node.
30+
*/
2231
// deno-lint-ignore no-explicit-any
2332
export interface Type<K extends KindType, D = any> {
33+
/** Tag to identify the type */
2434
tag: string;
35+
/** Kind of type */
2536
kind: K;
37+
/** Cast the type. Used to stringify */
2638
predicate?: (data: unknown) => data is D;
39+
/** Function to represent data. Used to stringify */
2740
represent?: RepresentFn<D> | Record<string, RepresentFn<D>>;
41+
/** Default style for the type. Used to stringify */
2842
defaultStyle?: StyleVariant;
43+
/** Function to test whether data can be resolved by this type. Used to parse */
2944
// deno-lint-ignore no-explicit-any
3045
resolve: (data: any) => boolean;
46+
/** Function to construct data from string. Used to parse */
3147
// deno-lint-ignore no-explicit-any
3248
construct: (data: any) => D;
3349
}

yaml/deno.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
".": "./mod.ts",
66
"./parse": "./parse.ts",
77
"./stringify": "./stringify.ts",
8-
"./unstable-stringify": "./unstable_stringify.ts"
8+
"./unstable-stringify": "./unstable_stringify.ts",
9+
"./unstable-parse": "./unstable_parse.ts"
910
}
1011
}

yaml/parse_test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Copyright 2018-2025 the Deno authors. MIT license.
55

66
import { parse, parseAll } from "./parse.ts";
7+
import { type ImplicitType, parse as unstableParse } from "./unstable_parse.ts";
78
import {
89
assert,
910
assertEquals,
@@ -492,6 +493,36 @@ Deno.test({
492493
},
493494
});
494495

496+
Deno.test({
497+
name: "unstableParse() handles custom types",
498+
fn() {
499+
const foo: ImplicitType = {
500+
tag: "tag:custom:smile",
501+
resolve: (data: string): boolean => data === "=)",
502+
construct: (): string => "🙂",
503+
predicate: (data: unknown): data is string => data === "🙂",
504+
kind: "scalar",
505+
represent: (): string => "=)",
506+
};
507+
508+
assertEquals(
509+
unstableParse(
510+
`
511+
title: =)
512+
tags:
513+
- =)
514+
- bar
515+
`,
516+
{ extraTypes: [foo] },
517+
),
518+
{
519+
title: "🙂",
520+
tags: ["🙂", "bar"],
521+
},
522+
);
523+
},
524+
});
525+
495526
Deno.test("parse() handles !!pairs type", () => {
496527
assertEquals(
497528
parse(`!!pairs

yaml/stringify_test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import { assertEquals, assertThrows } from "@std/assert";
77
import { stringify } from "./stringify.ts";
8-
import { stringify as unstableStringify } from "./unstable_stringify.ts";
8+
import {
9+
type ImplicitType,
10+
stringify as unstableStringify,
11+
} from "./unstable_stringify.ts";
912
import { compare, parse } from "@std/semver";
1013

1114
Deno.test({
@@ -867,3 +870,29 @@ Deno.test({
867870
);
868871
},
869872
});
873+
874+
Deno.test({
875+
name: "unstableStringify() handles custom types",
876+
fn() {
877+
const foo: ImplicitType = {
878+
tag: "tag:custom:smile",
879+
resolve: (data: string): boolean => data === "=)",
880+
construct: (): string => "🙂",
881+
predicate: (data: unknown): data is string => data === "🙂",
882+
kind: "scalar",
883+
represent: (): string => "=)",
884+
};
885+
886+
assertEquals(
887+
unstableStringify({
888+
title: "🙂",
889+
tags: ["🙂", "bar"],
890+
}, { extraTypes: [foo] }),
891+
`title: =)
892+
tags:
893+
- =)
894+
- bar
895+
`,
896+
);
897+
},
898+
});

yaml/unstable_parse.ts

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Ported from js-yaml v3.13.1:
2+
// https://github.com/nodeca/js-yaml/commit/665aadda42349dcae869f12040d9b10ef18d12da
3+
// Copyright 2011-2015 by Vitaly Puzrin. All rights reserved. MIT license.
4+
// Copyright 2018-2025 the Deno authors. MIT license.
5+
// This module is browser compatible.
6+
7+
import { isEOL } from "./_chars.ts";
8+
import { LoaderState } from "./_loader_state.ts";
9+
import type { ParseOptions as StableParseOptions } from "./parse.ts";
10+
import { getSchema, type ImplicitType, type SchemaType } from "./_schema.ts";
11+
import type { KindType, RepresentFn, Type } from "./_type.ts";
12+
13+
export type { ImplicitType, KindType, RepresentFn, SchemaType, Type };
14+
15+
/** Options for {@linkcode parse}. */
16+
export type ParseOptions = StableParseOptions & {
17+
/**
18+
* Extra types to be added to the schema.
19+
*/
20+
extraTypes?: ImplicitType[];
21+
};
22+
23+
function sanitizeInput(input: string) {
24+
input = String(input);
25+
26+
if (input.length > 0) {
27+
// Add trailing `\n` if not exists
28+
if (!isEOL(input.charCodeAt(input.length - 1))) input += "\n";
29+
30+
// Strip BOM
31+
if (input.charCodeAt(0) === 0xfeff) input = input.slice(1);
32+
}
33+
34+
// Use 0 as string terminator. That significantly simplifies bounds check.
35+
input += "\0";
36+
37+
return input;
38+
}
39+
40+
/**
41+
* Parse and return a YAML string as a parsed YAML document object.
42+
*
43+
* Note: This does not support functions. Untrusted data is safe to parse.
44+
*
45+
* @example Usage
46+
* ```ts
47+
* import { parse } from "@std/yaml/parse";
48+
* import { assertEquals } from "@std/assert";
49+
*
50+
* const data = parse(`
51+
* id: 1
52+
* name: Alice
53+
* `);
54+
*
55+
* assertEquals(data, { id: 1, name: "Alice" });
56+
* ```
57+
*
58+
* @throws {SyntaxError} Throws error on invalid YAML.
59+
* @param content YAML string to parse.
60+
* @param options Parsing options.
61+
* @returns Parsed document.
62+
*/
63+
export function parse(
64+
content: string,
65+
options: ParseOptions = {},
66+
): unknown {
67+
content = sanitizeInput(content);
68+
const state = new LoaderState(content, {
69+
...options,
70+
schema: getSchema(options.schema, options.extraTypes),
71+
});
72+
const documentGenerator = state.readDocuments();
73+
const document = documentGenerator.next().value;
74+
if (!documentGenerator.next().done) {
75+
throw new SyntaxError(
76+
"Found more than 1 document in the stream: expected a single document",
77+
);
78+
}
79+
return document ?? null;
80+
}
81+
82+
/**
83+
* Same as {@linkcode parse}, but understands multi-document YAML sources, and
84+
* returns multiple parsed YAML document objects.
85+
*
86+
* @example Usage
87+
* ```ts
88+
* import { parseAll } from "@std/yaml/parse";
89+
* import { assertEquals } from "@std/assert";
90+
*
91+
* const data = parseAll(`
92+
* ---
93+
* id: 1
94+
* name: Alice
95+
* ---
96+
* id: 2
97+
* name: Bob
98+
* ---
99+
* id: 3
100+
* name: Eve
101+
* `);
102+
* assertEquals(data, [ { id: 1, name: "Alice" }, { id: 2, name: "Bob" }, { id: 3, name: "Eve" }]);
103+
* ```
104+
*
105+
* @param content YAML string to parse.
106+
* @param options Parsing options.
107+
* @returns Array of parsed documents.
108+
*/
109+
export function parseAll(content: string, options: ParseOptions = {}): unknown {
110+
content = sanitizeInput(content);
111+
const state = new LoaderState(content, {
112+
...options,
113+
schema: getSchema(options.schema, options.extraTypes),
114+
});
115+
return [...state.readDocuments()];
116+
}

yaml/unstable_stringify.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
// This module is browser compatible.
66

77
import { DumperState } from "./_dumper_state.ts";
8-
import { SCHEMA_MAP } from "./_schema.ts";
8+
import { getSchema, type ImplicitType, type SchemaType } from "./_schema.ts";
99
import type { StringifyOptions as StableStringifyOptions } from "./stringify.ts";
10+
import type { KindType, RepresentFn, Type } from "./_type.ts";
11+
12+
export type { ImplicitType, KindType, RepresentFn, SchemaType, Type };
1013

1114
/** Options for {@linkcode stringify}. */
1215
export type StringifyOptions = StableStringifyOptions & {
@@ -18,6 +21,11 @@ export type StringifyOptions = StableStringifyOptions & {
1821
* @default {`'`}
1922
*/
2023
quoteStyle?: "'" | '"';
24+
25+
/**
26+
* Extra types to be added to the schema.
27+
*/
28+
extraTypes?: ImplicitType[];
2129
};
2230

2331
/**
@@ -45,7 +53,7 @@ export function stringify(
4553
): string {
4654
const state = new DumperState({
4755
...options,
48-
schema: SCHEMA_MAP.get(options.schema!)!,
56+
schema: getSchema(options.schema, options.extraTypes),
4957
});
5058
return state.stringify(data);
5159
}

0 commit comments

Comments
 (0)