Skip to content

Commit

Permalink
feat: Allow for Schema instance as schema option (#344)
Browse files Browse the repository at this point in the history
* Allow for Schema instance as schema option
* Disable anchors & aliases if doc.directives is empty
  • Loading branch information
eemeli authored Dec 31, 2021
1 parent e67a221 commit 6943654
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 33 deletions.
50 changes: 32 additions & 18 deletions src/doc/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type Replacer = any[] | ((key: any, value: any) => unknown)

export declare namespace Document {
interface Parsed<T extends ParsedNode = ParsedNode> extends Document<T> {
directives: Directives
range: Range
}
}
Expand All @@ -53,7 +54,7 @@ export class Document<T = unknown> {
/** The document contents. */
contents: T | null

directives: Directives
directives?: Directives

/** Errors encountered during parsing. */
errors: YAMLError[] = []
Expand Down Expand Up @@ -139,7 +140,7 @@ export class Document<T = unknown> {
copy.errors = this.errors.slice()
copy.warnings = this.warnings.slice()
copy.options = Object.assign({}, this.options)
copy.directives = this.directives.clone()
if (this.directives) copy.directives = this.directives.clone()
copy.schema = this.schema.clone()
copy.contents = isNode(this.contents)
? (this.contents.clone(copy.schema) as unknown as T)
Expand Down Expand Up @@ -346,32 +347,45 @@ export class Document<T = unknown> {

/**
* Change the YAML version and schema used by the document.
* A `null` version disables support for directives, explicit tags, anchors, and aliases.
* It also requires the `schema` option to be given as a `Schema` instance value.
*
* Overrides all previously set schema options
* Overrides all previously set schema options.
*/
setSchema(version: '1.1' | '1.2', options?: SchemaOptions) {
let _options: SchemaOptions
switch (String(version)) {
setSchema(version: '1.1' | '1.2' | null, options: SchemaOptions = {}) {
if (typeof version === 'number') version = String(version) as '1.1' | '1.2'

let opt: (SchemaOptions & { schema: string }) | null
switch (version) {
case '1.1':
this.directives.yaml.version = '1.1'
_options = Object.assign(
{ merge: true, resolveKnownTags: false, schema: 'yaml-1.1' },
options
)
if (this.directives) this.directives.yaml.version = '1.1'
else this.directives = new Directives({ version: '1.1' })
opt = { merge: true, resolveKnownTags: false, schema: 'yaml-1.1' }
break
case '1.2':
this.directives.yaml.version = '1.2'
_options = Object.assign(
{ merge: false, resolveKnownTags: true, schema: 'core' },
options
)
if (this.directives) this.directives.yaml.version = '1.2'
else this.directives = new Directives({ version: '1.2' })
opt = { merge: false, resolveKnownTags: true, schema: 'core' }
break
case null:
if (this.directives) delete this.directives
opt = null
break
default: {
const sv = JSON.stringify(version)
throw new Error(`Expected '1.1' or '1.2' as version, but found: ${sv}`)
throw new Error(
`Expected '1.1', '1.2' or null as first argument, but found: ${sv}`
)
}
}
this.schema = new Schema(_options)

// Not using `instanceof Schema` to allow for duck typing
if (options.schema instanceof Object) this.schema = options.schema
else if (opt) this.schema = new Schema(Object.assign(opt, options))
else
throw new Error(
`With a null YAML version, the { schema: Schema } option is required`
)
}

/** A plain JavaScript representation of the document `contents`. */
Expand Down
3 changes: 2 additions & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { ParsedNode } from './nodes/Node.js'
import type { Pair } from './nodes/Pair.js'
import type { Scalar } from './nodes/Scalar.js'
import type { LineCounter } from './parse/line-counter.js'
import type { Schema } from './schema/Schema.js'
import type { Tags } from './schema/tags.js'
import type { CollectionTag, ScalarTag } from './schema/types.js'

Expand Down Expand Up @@ -131,7 +132,7 @@ export type SchemaOptions = {
*
* Default: `'core'` for YAML 1.2, `'yaml-1.1'` for earlier versions
*/
schema?: string
schema?: string | Schema

/**
* When adding to or stringifying a map, sort the entries.
Expand Down
8 changes: 3 additions & 5 deletions src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,10 @@ export interface EmptyStream
empty: true
}

function parseOptions(options: ParseOptions | undefined) {
const prettyErrors = !options || options.prettyErrors !== false
function parseOptions(options: ParseOptions) {
const prettyErrors = options.prettyErrors !== false
const lineCounter =
(options && options.lineCounter) ||
(prettyErrors && new LineCounter()) ||
null
options.lineCounter || (prettyErrors && new LineCounter()) || null
return { lineCounter, prettyErrors }
}

Expand Down
2 changes: 1 addition & 1 deletion src/schema/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class Schema {
? getTags(null, compat)
: null
this.merge = !!merge
this.name = schema || 'core'
this.name = (typeof schema === 'string' && schema) || 'core'
this.knownTags = resolveKnownTags ? coreKnownTags : {}
this.tags = getTags(customTags, this.name)
this.toStringOptions = toStringDefaults || null
Expand Down
24 changes: 18 additions & 6 deletions src/stringify/stringify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { anchorIsValid } from '../doc/anchors.js'
import type { Document } from '../doc/Document.js'
import type { Alias } from '../nodes/Alias.js'
import {
isAlias,
isCollection,
Expand Down Expand Up @@ -29,6 +30,7 @@ export type StringifyContext = {
options: Readonly<
Required<Omit<ToStringOptions, 'collectionStyle' | 'indent'>>
>
resolvedAliases?: Set<Alias>
}

export function createStringifyContext(
Expand Down Expand Up @@ -113,17 +115,15 @@ function stringifyProps(
tagObj: ScalarTag | CollectionTag,
{ anchors, doc }: StringifyContext
) {
if (!doc.directives) return ''
const props = []
const anchor = (isScalar(node) || isCollection(node)) && node.anchor
if (anchor && anchorIsValid(anchor)) {
anchors.add(anchor)
props.push(`&${anchor}`)
}
if (node.tag) {
props.push(doc.directives.tagString(node.tag))
} else if (!tagObj.default) {
props.push(doc.directives.tagString(tagObj.tag))
}
const tag = node.tag || (tagObj.default ? null : tagObj.tag)
if (tag) props.push(doc.directives.tagString(tag))
return props.join(' ')
}

Expand All @@ -134,7 +134,19 @@ export function stringify(
onChompKeep?: () => void
): string {
if (isPair(item)) return item.toString(ctx, onComment, onChompKeep)
if (isAlias(item)) return item.toString(ctx)
if (isAlias(item)) {
if (ctx.doc.directives) return item.toString(ctx)

if (ctx.resolvedAliases?.has(item)) {
throw new TypeError(
`Cannot stringify circular structure without alias nodes`
)
} else {
if (ctx.resolvedAliases) ctx.resolvedAliases.add(item)
else ctx.resolvedAliases = new Set([item])
item = item.resolve(ctx.doc)
}
}

let tagObj: ScalarTag | CollectionTag | undefined = undefined
const node = isNode(item)
Expand Down
2 changes: 1 addition & 1 deletion src/stringify/stringifyDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export function stringifyDocument(
) {
const lines: string[] = []
let hasDirectives = options.directives === true
if (options.directives !== false) {
if (options.directives !== false && doc.directives) {
const dir = doc.directives.toString(doc)
if (dir) {
lines.push(dir)
Expand Down
45 changes: 44 additions & 1 deletion tests/doc/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -775,7 +775,7 @@ describe('custom tags', () => {
expect(str).toBe('re: !re /re/g\nsymbol: !symbol/shared foo\n')
})

describe('completely custom schema', () => {
describe('schema from custom tags', () => {
test('customTags is required', () => {
expect(() =>
YAML.parseDocument('foo', { schema: 'custom-test' })
Expand Down Expand Up @@ -836,6 +836,23 @@ describe('custom tags', () => {
})
expect(() => String(doc)).toThrow(/Tag not resolved for String value/)
})

test('setSchema', () => {
const src = '- foo\n'
const doc = YAML.parseDocument(src)

doc.setSchema('1.2', {
customTags: [seqTag, stringTag],
schema: 'custom-test-1'
})
expect(doc.toString()).toBe(src)

doc.setSchema('1.2', {
customTags: [stringTag],
schema: 'custom-test-2'
})
expect(() => String(doc)).toThrow(/Tag not resolved for YAMLSeq value/)
})
})
})

Expand Down Expand Up @@ -904,4 +921,30 @@ describe('schema changes', () => {
doc.set('a', false)
expect(String(doc)).toBe('a: false\n')
})

test('custom schema instance', () => {
const src = '[ !!bool yes, &foo no, *foo ]'
const doc = YAML.parseDocument(src, { version: '1.1' })

doc.setSchema('1.2', new YAML.Schema({ schema: 'core' }))
expect(String(doc)).toBe('[ !!bool true, &foo false, *foo ]\n')

const yaml11 = new YAML.Schema({ schema: 'yaml-1.1' })
doc.setSchema('1.1', Object.assign({}, yaml11))
expect(String(doc)).toBe('[ !!bool yes, &foo no, *foo ]\n')

const schema = new YAML.Schema({ schema: 'core' })
doc.setSchema(null, { schema })
expect(String(doc)).toBe('[ true, false, false ]\n')
})

test('null version requires Schema instance', () => {
const doc = YAML.parseDocument('foo: bar')
const msg =
'With a null YAML version, the { schema: Schema } option is required'
expect(() => doc.setSchema(null)).toThrow(msg)
expect(() => doc.setSchema(null, { schema: 'core' })).toThrow(msg)
doc.setSchema(null, { schema: doc.schema })
expect(doc.toString()).toBe('foo: bar\n')
})
})

0 comments on commit 6943654

Please sign in to comment.