Repository: eemeli/yaml
Pull request #325 (2021-11-01 09:20:52)

Fixes & exports for custom composers
While developing a `yaml` based toolchain for working with other JSON variants, I've noted that a few necessary or useful things aren't properly exposed by the library; this adds those, including exported TS types for `'yaml/util'`.

As a new feature, it's now possible to define a completely custom schema, using your own string identifier for it.


Patch:
diff --git a/docs/03_options.md b/docs/03_options.md
index e5d4684..eb6b810 100644
--- a/docs/03_options.md
+++ b/docs/03_options.md
@@ -71,13 +71,13 @@ The `!!value` and `!!yaml` types are not supported.
 
 Used by: `parse()`, `parseDocument()`, `parseAllDocuments()`, `stringify()`, `new Composer()`, `new Document()`, and `doc.setSchema()`
 
-| Name             | Type                                          | Default                                  | Description                                                                                                                                                             |
-| ---------------- | --------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| customTags       | `Tag[] ⎮ function`                            |                                          | Array of [additional tags](#custom-data-types) to include in the schema                                                                                                 |
-| merge            | `boolean`                                     | 1.1:&nbsp;`true` 1.2:&nbsp;`false`       | Enable support for `<<` merge keys. Default value depends on YAML version.                                                                                              |
-| resolveKnownTags | `boolean`                                     | `true`                                   | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. |
-| schema           | `'core' ⎮ 'failsafe' ⎮` `'json' ⎮ 'yaml-1.1'` | 1.1:&nbsp;`'yaml-1.1` 1.2:&nbsp;`'core'` | The base schema to use. Default value depends on YAML version.                                                                                                          |
-| sortMapEntries   | `boolean ⎮` `(a, b: Pair) => number`          | `false`                                  | When stringifying, sort map entries. If `true`, sort by comparing key values using the native less-than `<` operator.                                                   |
+| Name             | Type                                                   | Default                                  | Description                                                                                                                                                             |
+| ---------------- | ------------------------------------------------------ | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| customTags       | `Tag[] ⎮ function`                                     |                                          | Array of [additional tags](#custom-data-types) to include in the schema                                                                                                 |
+| merge            | `boolean`                                              | 1.1:&nbsp;`true` 1.2:&nbsp;`false`       | Enable support for `<<` merge keys. Default value depends on YAML version.                                                                                              |
+| resolveKnownTags | `boolean`                                              | `true`                                   | When using the `'core'` schema, support parsing values with these explicit [YAML 1.1 tags]: `!!binary`, `!!omap`, `!!pairs`, `!!set`, `!!timestamp`. By default `true`. |
+| schema           | `'core' ⎮ 'failsafe' ⎮` `'json' ⎮ 'yaml-1.1' ⎮ string` | 1.1:&nbsp;`'yaml-1.1` 1.2:&nbsp;`'core'` | The base schema to use. Default value depends on YAML version. If using a custom value, `customTags` must be an array of tags.                                          |
+| sortMapEntries   | `boolean ⎮` `(a, b: Pair) => number`                   | `false`                                  | When stringifying, sort map entries. If `true`, sort by comparing key values using the native less-than `<` operator.                                                   |
 
 [yaml 1.1 tags]: https://yaml.org/type/
 
diff --git a/docs/06_custom_tags.md b/docs/06_custom_tags.md
index 85b7331..9b0d81c 100644
--- a/docs/06_custom_tags.md
+++ b/docs/06_custom_tags.md
@@ -137,6 +137,9 @@ import {
   debug, // (logLevel, ...messages) => void -- Log debug messages to console
   findPair, // (items, key) => Pair? -- Given a key, find a matching Pair
   foldFlowLines, // (text, indent, mode, options) => string -- Fold long lines
+  mapTag, // CollectionTag
+  seqTag, // CollectionTag
+  stringTag, // ScalarTag
   stringifyNumber, // (node) => string
   stringifyString, // (node, ctx, ...) => string
   toJS, // (value, arg, ctx) => any -- Recursively convert to plain JS
diff --git a/package.json b/package.json
index a026aab..795dfc0 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
   "files": [
     "browser/",
     "dist/",
+    "util.d.ts",
     "util.js"
   ],
   "type": "commonjs",
diff --git a/src/index.ts b/src/index.ts
index 50bd1b1..9628131 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -3,7 +3,7 @@ export { Composer } from './compose/composer.js'
 export { Document } from './doc/Document.js'
 export { Schema } from './schema/Schema.js'
 
-export { YAMLError, YAMLParseError, YAMLWarning } from './errors.js'
+export { ErrorCode, YAMLError, YAMLParseError, YAMLWarning } from './errors.js'
 
 export { Alias } from './nodes/Alias.js'
 export {
@@ -16,7 +16,8 @@ export {
   isScalar,
   isSeq,
   Node,
-  ParsedNode
+  ParsedNode,
+  Range
 } from './nodes/Node.js'
 export { Pair } from './nodes/Pair.js'
 export { Scalar } from './nodes/Scalar.js'
diff --git a/src/schema/tags.ts b/src/schema/tags.ts
index 500ba06..abd1f90 100644
--- a/src/schema/tags.ts
+++ b/src/schema/tags.ts
@@ -63,11 +63,16 @@ export function getTags(
 ) {
   let tags: Tags = schemas[schemaName]
   if (!tags) {
-    const keys = Object.keys(schemas)
-      .filter(key => key !== 'yaml11')
-      .map(key => JSON.stringify(key))
-      .join(', ')
-    throw new Error(`Unknown schema "${schemaName}"; use one of ${keys}`)
+    if (Array.isArray(customTags)) tags = []
+    else {
+      const keys = Object.keys(schemas)
+        .filter(key => key !== 'yaml11')
+        .map(key => JSON.stringify(key))
+        .join(', ')
+      throw new Error(
+        `Unknown schema "${schemaName}"; use one of ${keys} or define customTags array`
+      )
+    }
   }
 
   if (Array.isArray(customTags)) {
diff --git a/src/util.ts b/src/util.ts
index 6ba8320..954b0f7 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1,6 +1,9 @@
 export { debug, LogLevelId, warn } from './log.js'
 export { findPair } from './nodes/YAMLMap.js'
 export { toJS, ToJSContext } from './nodes/toJS.js'
+export { map as mapTag } from './schema/common/map.js'
+export { seq as seqTag } from './schema/common/seq.js'
+export { string as stringTag } from './schema/common/string.js'
 export { foldFlowLines } from './stringify/foldFlowLines'
 export { stringifyNumber } from './stringify/stringifyNumber.js'
 export { stringifyString } from './stringify/stringifyString.js'
diff --git a/util.d.ts b/util.d.ts
new file mode 100644
index 0000000..c656480
--- /dev/null
+++ b/util.d.ts
@@ -0,0 +1,3 @@
+// Workaround for incomplete exports support in TypeScript
+// https://github.com/microsoft/TypeScript/issues/33079
+export * from './dist/util.js'


Test patch:
diff --git a/tests/doc/types.js b/tests/doc/types.js
index fde297f..ffb6eb1 100644
--- a/tests/doc/types.js
+++ b/tests/doc/types.js
@@ -1,6 +1,6 @@
 import * as YAML from 'yaml'
 import { Scalar, YAMLSeq } from 'yaml'
-import { stringifyString } from 'yaml/util'
+import { seqTag, stringTag, stringifyString } from 'yaml/util'
 import { source } from '../_utils'
 
 describe('json schema', () => {
@@ -774,6 +774,69 @@ describe('custom tags', () => {
     const str = YAML.stringify(obj, { customTags: [[regexp, sharedSymbol]] })
     expect(str).toBe('re: !re /re/g\nsymbol: !symbol/shared foo\n')
   })
+
+  describe('completely custom schema', () => {
+    test('customTags is required', () => {
+      expect(() =>
+        YAML.parseDocument('foo', { schema: 'custom-test' })
+      ).toThrow(/Unknown schema "custom-test"/)
+    })
+
+    test('parse success', () => {
+      const src = '- foo\n- !re /bar/\n'
+      const doc = YAML.parseDocument(src, {
+        customTags: [seqTag, stringTag, regexp],
+        schema: 'custom-test'
+      })
+      expect(doc.errors).toEqual([])
+      expect(doc.warnings).toEqual([])
+      expect(doc.schema.name).toBe('custom-test')
+      expect(doc.toJS()).toEqual(['foo', /bar/])
+    })
+
+    test('parse fail', () => {
+      // map, seq & string are always parsed, even if not included
+      const src = '- foo\n- !re /bar/\n'
+      const doc = YAML.parseDocument(src, {
+        customTags: [],
+        schema: 'custom-test'
+      })
+      expect(doc.errors).toEqual([])
+      expect(doc.warnings).toHaveLength(1)
+      expect(doc.warnings[0].message).toMatch(/Unresolved tag: !re/)
+    })
+
+    test('stringify success', () => {
+      let src = '- foo\n'
+      let doc = YAML.parseDocument(src, {
+        customTags: [seqTag, stringTag],
+        schema: 'custom-test'
+      })
+      expect(doc.toString()).toBe(src)
+
+      src = '- foo\n- !re /bar/\n'
+      doc = YAML.parseDocument(src, {
+        customTags: [seqTag, stringTag, regexp],
+        schema: 'custom-test'
+      })
+      expect(doc.toString()).toBe(src)
+    })
+
+    test('stringify fail', () => {
+      const src = '- foo\n'
+      let doc = YAML.parseDocument(src, {
+        customTags: [],
+        schema: 'custom-test'
+      })
+      expect(() => String(doc)).toThrow(/Tag not resolved for YAMLSeq value/)
+
+      doc = YAML.parseDocument(src, {
+        customTags: [seqTag],
+        schema: 'custom-test'
+      })
+      expect(() => String(doc)).toThrow(/Tag not resolved for String value/)
+    })
+  })
 })
 
 describe('schema changes', () => {