Skip to content

Commit

Permalink
Merge pull request #299 from eemeli/optional-aliases
Browse files Browse the repository at this point in the history
Add a new createNode option aliasDuplicateObjects
  • Loading branch information
eemeli committed Aug 27, 2021
2 parents 1654765 + aa724ec commit 49753a0
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 20 deletions.
13 changes: 7 additions & 6 deletions docs/03_options.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,13 @@ Multiple merge keys may be used on the same map, with earlier values taking prec

Used by: `stringify()`, `new Document()`, `doc.createNode()`, and `doc.createPair()`

| Name | Type | Default | Description |
| ------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| anchorPrefix | `string` | `'a'` | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default. |
| flow | `boolean` | `false` | Force the top-level collection node to use flow style. |
| keepUndefined | `boolean` | `false` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. |
| tag | `string` | | Specify the top-level collection type, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available in this document's schema. |
| Name | Type | Default | Description |
| --------------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| aliasDuplicateObjects | `boolean` | `true` | During node construction, use anchors and aliases to keep strictly equal non-null objects as equivalent in YAML. |
| anchorPrefix | `string` | `'a'` | Default prefix for anchors, resulting in anchors `a1`, `a2`, ... by default. |
| flow | `boolean` | `false` | Force the top-level collection node to use flow style. |
| keepUndefined | `boolean` | `false` | Keep `undefined` object values when creating mappings and return a Scalar node when stringifying `undefined`. |
| tag | `string` | | Specify the top-level collection type, e.g. `"!!omap"`. Note that this requires the corresponding tag to be available in this document's schema. |

## ToJS Options

Expand Down
10 changes: 9 additions & 1 deletion src/doc/Document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,12 +182,20 @@ export class Document<T = unknown> {
replacer = undefined
}

const { anchorPrefix, flow, keepUndefined, onTagObj, tag } = options || {}
const {
aliasDuplicateObjects,
anchorPrefix,
flow,
keepUndefined,
onTagObj,
tag
} = options || {}
const { onAnchor, setAnchors, sourceObjects } = createNodeAnchors(
this,
anchorPrefix || 'a'
)
const ctx: CreateNodeContext = {
aliasDuplicateObjects: aliasDuplicateObjects ?? true,
keepUndefined: keepUndefined ?? false,
onAnchor,
onTagObj,
Expand Down
8 changes: 5 additions & 3 deletions src/doc/createNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type { Replacer } from './Document.js'
const defaultTagPrefix = 'tag:yaml.org,2002:'

export interface CreateNodeContext {
keepUndefined?: boolean
aliasDuplicateObjects: boolean
keepUndefined: boolean
onAnchor(source: unknown): string
onTagObj?: (tagObj: ScalarTag | CollectionTag) => void
sourceObjects: Map<unknown, { anchor: string | null; node: Node | null }>
Expand Down Expand Up @@ -52,12 +53,13 @@ export function createNode(
value = value.valueOf()
}

const { onAnchor, onTagObj, schema, sourceObjects } = ctx
const { aliasDuplicateObjects, onAnchor, onTagObj, schema, sourceObjects } =
ctx

// Detect duplicate references to the same object & use Alias nodes for all
// after first. The `ref` wrapper allows for circular references to resolve.
let ref: { anchor: string | null; node: Node | null } | undefined = undefined
if (value && typeof value === 'object') {
if (aliasDuplicateObjects && value && typeof value === 'object') {
ref = sourceObjects.get(value)
if (ref) {
if (!ref.anchor) ref.anchor = onAnchor(value)
Expand Down
15 changes: 5 additions & 10 deletions src/nodes/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,14 @@ export function collectionFromPath(
a[k] = v
v = a
} else {
const o = {}
Object.defineProperty(o, typeof k === 'symbol' ? k : String(k), {
value: v,
writable: true,
enumerable: true,
configurable: true
})
v = o
v = new Map<unknown, unknown>([[k, v]])
}
}
return createNode(v, undefined, {
onAnchor() {
throw new Error('Repeated objects are not supported here')
aliasDuplicateObjects: false,
keepUndefined: false,
onAnchor: () => {
throw new Error('This should not happen, please report a bug.')
},
schema,
sourceObjects: new Map()
Expand Down
8 changes: 8 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ export type SchemaOptions = {
}

export type CreateNodeOptions = {
/**
* During node construction, use anchors and aliases to keep strictly equal
* non-null objects as equivalent in YAML.
*
* Default: `true`
*/
aliasDuplicateObjects?: boolean

/**
* Default prefix for anchors.
*
Expand Down
33 changes: 33 additions & 0 deletions tests/doc/collection-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,4 +520,37 @@ describe('Document', () => {
doc.setIn(['c', '__proto__'], 9)
expect(String(doc)).toBe('a: 1\nb:\n - 2\n - 3\nc:\n __proto__: 9\n')
})

test('setIn with object key', () => {
doc.contents = null
const foo = { foo: 'FOO' }
doc.setIn([foo], 'BAR')
expect(doc.contents.items).toMatchObject([
{
key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] },
value: { value: 'BAR' }
}
])
})

test('setIn with repeated object key', () => {
doc.contents = null
const foo = { foo: 'FOO' }
doc.setIn([foo, foo], 'BAR')
expect(doc.contents.items).toMatchObject([
{
key: { items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] },
value: {
items: [
{
key: {
items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }]
},
value: { value: 'BAR' }
}
]
}
}
])
})
})
25 changes: 25 additions & 0 deletions tests/doc/createNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,31 @@ describe('toJSON()', () => {
})
})

describe('strictly equal objects', () => {
test('createNode([foo, foo])', () => {
const foo = { foo: 'FOO' }
const s = doc.createNode([foo, foo])
expect(s).toBeInstanceOf(YAMLSeq)
expect(s.items).toMatchObject([
{
anchor: 'a1',
items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }]
},
{ source: 'a1' }
])
})

test('createNode([foo, foo], { aliasDuplicateObjects: false })', () => {
const foo = { foo: 'FOO' }
const s = doc.createNode([foo, foo], { aliasDuplicateObjects: false })
expect(s).toBeInstanceOf(YAMLSeq)
expect(s.items).toMatchObject([
{ items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] },
{ items: [{ key: { value: 'foo' }, value: { value: 'FOO' } }] }
])
})
})

describe('circular references', () => {
test('parent at root', () => {
const map = { foo: 'bar' }
Expand Down

0 comments on commit 49753a0

Please sign in to comment.