Skip to content

Commit

Permalink
feat: support tree structure (ad buffet)
Browse files Browse the repository at this point in the history
  • Loading branch information
delbeke committed Mar 22, 2017
1 parent 9174f62 commit 2c02b99
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 60 deletions.
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ const loader = new VASTLoader(tagUrl)

// Load the tag chain and await the resulting Promise
loader.load()
.then((chain) => {
console.info('Loaded VAST tags:', chain)
.then((tree) => {
console.info('Loaded VAST tags:', tree)
})
.catch((err) => {
console.error('Error loading tag:', err)
Expand All @@ -36,8 +36,19 @@ Creates a VAST loader.
loader.load()
```

Returns a `Promise` for an array of `VAST` instances. The `VAST` class is
provided by [iab-vast-model](https://www.npmjs.com/package/iab-vast-model).
Returns a `Promise` for a `VASTTreeNode` instance.
A `VASTTreeNode` has a property called `vast` which contains the loaded VAST document, parsed into a `VAST` class.
This `VAST` class is provided by [iab-vast-model](https://www.npmjs.com/package/iab-vast-model).
The `VASTTreeNode` also has a property called `childNodes` that contains an array of "sub ads", again instances of `VASTTreeNode`.

## VASTTreeNode

```js
node.vast // the VAST document
node.childNodes // array of VASTTreeNode
node.firstChild // first element in the childNodes array
node.hasChildNodes() // function that tells you if any sub ads where present
```

## Error Handling

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"globals": [
"describe",
"it",
"xit",
"before",
"after",
"beforeEach",
Expand Down
28 changes: 18 additions & 10 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EventEmitter from 'eventemitter3'
import fetch from 'isomorphic-fetch'
import parse from 'iab-vast-parser'
import { Wrapper } from 'iab-vast-model'
import VASTTreeNode from './vast-tree-node'
import VASTLoaderError from './error'
import atob from './atob'

Expand Down Expand Up @@ -44,24 +45,34 @@ export default class Loader extends EventEmitter {
.then(() => {
this._emit('willFetch', { uri })
const match = RE_DATA_URI.exec(uri)
return (match == null) ? this._fetchUri()
: (match[1] != null) ? atob(match[2])
: decodeURIComponent(match[2])
if (match == null) {
return this._fetchUri()
} else if (match[1] != null) {
return atob(match[2])
} else {
return decodeURIComponent(match[2])
}
})
.then((body) => {
this._emit('didFetch', { uri, body })
this._emit('willParse', { uri, body })
const vast = parse(body)
this._emit('didParse', { uri, body, vast })
if (vast.ads.length > 0) {
const ad = vast.ads.get(0)
if (ad instanceof Wrapper || ad.$type === 'Wrapper') {
return this._loadWrapped(ad.vastAdTagURI, vast)
const adsToLoad = []
for (var i = 0; i < vast.ads.length; i++) {
var ad = vast.ads.get(i)
if (ad instanceof Wrapper || ad.$type === 'Wrapper') {
adsToLoad.push(this._loadWrapped(ad.vastAdTagURI, vast))
}
}
return Promise.all(adsToLoad).then((ads) => {
return new VASTTreeNode(vast, ads)
})
} else if (this._depth > 1) {
throw new VASTLoaderError(303)
}
return [vast]
return new VASTTreeNode(vast)
})
}

Expand All @@ -75,9 +86,6 @@ export default class Loader extends EventEmitter {
const childLoader = new Loader(vastAdTagURI, null, this)
return childLoader.load()
})
.then((children) => {
return [vast, ...children]
})
}

_fetchUri () {
Expand Down
22 changes: 22 additions & 0 deletions src/vast-tree-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export default class VASTTreeNode {
constructor (vast, children = []) {
this._vast = vast
this._children = children
}

get vast () {
return this._vast
}

get childNodes () {
return this._children
}

get firstChild () {
return this._children[0]
}

hasChildNodes () {
return this._children.length > 0
}
}
13 changes: 13 additions & 0 deletions test/fixtures/ads-wrapper-multi.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<VAST version="2.0">
<Ad>
<Wrapper>
<VASTAdTagURI>http://demo.tremormedia.com/proddev/vast/vast_inline_linear.xml</VASTAdTagURI>
</Wrapper>
</Ad>
<Ad>
<Wrapper>
<VASTAdTagURI>http://demo.tremormedia.com/proddev/vast/vast_inline_linear.xml</VASTAdTagURI>
</Wrapper>
</Ad>
</VAST>
2 changes: 2 additions & 0 deletions test/fixtures/no-ads-alt.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<VAST version="2.0"/>
32 changes: 32 additions & 0 deletions test/unit/vast-loader-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import VASTLoaderError from '../../src/error'

describe('VASTLoaderError', function () {
describe('#code', function () {
it('gets set from the constructor', function () {
const error = new VASTLoaderError(301)
expect(error.code).to.equal(301)
})
})

describe('#message', function () {
it('resolves from the code', function () {
const error = new VASTLoaderError(301)
expect(error.message).to.equal('Timeout.')
})
})

describe('#cause', function () {
it('gets set from the constructor', function () {
const cause = new Error('Foo')
const error = new VASTLoaderError(301, cause)
expect(error.cause).to.equal(cause)
})
})

describe('#$type', function () {
it('is VASTLoaderError', function () {
const error = new VASTLoaderError(900)
expect(error.$type).to.equal('VASTLoaderError')
})
})
})
74 changes: 28 additions & 46 deletions test/unit/node.js → test/unit/vast-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express'
import fsp from 'fs-promise'
import path from 'path'
import { default as VASTLoader, VASTLoaderError } from '../../src/'
import VASTTreeNode from '../../src/vast-tree-node'

const expectLoaderError = (error, code, message, cause) => {
expect(error).to.be.an.instanceof(VASTLoaderError)
Expand All @@ -12,42 +13,12 @@ const expectLoaderError = (error, code, message, cause) => {
}
}

describe('VASTLoaderError', function () {
describe('#code', function () {
it('gets set from the constructor', function () {
const error = new VASTLoaderError(301)
expect(error.code).to.equal(301)
})
})

describe('#message', function () {
it('resolves from the code', function () {
const error = new VASTLoaderError(301)
expect(error.message).to.equal('Timeout.')
})
})

describe('#cause', function () {
it('gets set from the constructor', function () {
const cause = new Error('Foo')
const error = new VASTLoaderError(301, cause)
expect(error.cause).to.equal(cause)
})
})

describe('#$type', function () {
it('is VASTLoaderError', function () {
const error = new VASTLoaderError(900)
expect(error.$type).to.equal('VASTLoaderError')
})
})
})

describe('VASTLoader', function () {
const fixturesPath = path.resolve(__dirname, '../fixtures')
const proxyPaths = {
'http://demo.tremormedia.com/proddev/vast/vast_inline_linear.xml': 'tremor-video/vast_inline_linear.xml',
'http://example.com/no-ads.xml': 'no-ads.xml',
'http://example.com/no-ads-alt.xml': 'no-ads-alt.xml',
'http://example.com/invalid-ads.xml': 'invalid-ads.xml'
}

Expand Down Expand Up @@ -104,43 +75,54 @@ describe('VASTLoader', function () {
describe('#load()', function () {
it('loads the InLine', async function () {
const loader = createLoader('tremor-video/vast_inline_linear.xml')
const chain = await loader.load()
expect(chain).to.be.an.instanceof(Array)
expect(chain.length).to.equal(1)
const tree = await loader.load()
expect(tree).to.be.an.instanceof(VASTTreeNode)
expect(tree.hasChildNodes()).to.be.false
})

it('loads the Wrapper', async function () {
const loader = createLoader('tremor-video/vast_wrapper_linear_1.xml')
const chain = await loader.load()
expect(chain).to.be.an.instanceof(Array)
expect(chain.length).to.equal(2)
const tree = await loader.load()
expect(tree).to.be.an.instanceof(VASTTreeNode)
expect(tree.hasChildNodes()).to.be.true
expect(tree.childNodes.length).to.equal(1)
})

xit('loads alls ads in a Wrapper', async function () {
// iab-vast-parser first needs to support ad buffets
const loader = createLoader('ads-wrapper-multi.xml')
const tree = await loader.load()
expect(tree).to.be.an.instanceof(VASTTreeNode)
expect(tree.hasChildNodes()).to.be.true
expect(tree.childNodes.length).to.equal(2)
expect(tree.childNodes[0]).to.equal(tree.firstChild)
})

it('loads the InLine as Base64', async function () {
const file = path.join(fixturesPath, 'tremor-video/vast_inline_linear.xml')
const base64 = (await fsp.readFile(file)).toString('base64')
const dataUri = 'data:text/xml;base64,' + base64
const loader = new VASTLoader(dataUri)
const chain = await loader.load()
expect(chain).to.be.an.instanceof(Array)
expect(chain.length).to.equal(1)
const tree = await loader.load()
expect(tree).to.be.an.instanceof(VASTTreeNode)
expect(tree.hasChildNodes()).to.be.false
})

it('loads the InLine as XML', async function () {
const file = path.join(fixturesPath, 'tremor-video/vast_inline_linear.xml')
const xml = (await fsp.readFile(file, 'utf8')).replace(/\r?\n/g, '')
const dataUri = 'data:text/xml,' + xml
const loader = new VASTLoader(dataUri)
const chain = await loader.load()
expect(chain).to.be.an.instanceof(Array)
expect(chain.length).to.equal(1)
const tree = await loader.load()
expect(tree).to.be.an.instanceof(VASTTreeNode)
expect(tree.hasChildNodes()).to.be.false
})

it('loads the empty tag', async function () {
const loader = createLoader('no-ads.xml')
const chain = await loader.load()
expect(chain.length).to.equal(1)
expect(chain[0].ads.length).to.equal(0)
const tree = await loader.load()
expect(tree.hasChildNodes()).to.be.false
expect(tree.vast.ads.length).to.equal(0)
})

it('throws VAST 303 on empty InLine inside Wrapper', async function () {
Expand Down

0 comments on commit 2c02b99

Please sign in to comment.