Skip to content
This repository has been archived by the owner on Aug 11, 2021. It is now read-only.

Commit

Permalink
feat: implementation of the new resolve() function
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `resolve()` replaces parts of `get()`.

The API docs for it:

> Retrieves IPLD Nodes along the `path` that is rooted at `cid`.

 - `cid` (`CID`, required): the CID the resolving starts.
 - `path` (`IPLD Path`, required): the path that should be resolved.

Returns an async iterator of all the IPLD Nodes that were traversed during the path resolving. Every element is an object with these fields:
 - `remainderPath` (`string`): the part of the path that wasn’t resolved yet.
 - `value` (`*`): the value where the resolved path points to. If further traversing is possible, then the value is a CID object linking to another IPLD Node. If it was possible to fully resolve the path, `value` is the value the `path` points to. So if you need the CID of the IPLD Node you’re currently at, just take the `value` of the previously returned IPLD Node.
  • Loading branch information
vmx committed Mar 21, 2019
1 parent af37805 commit 162473b
Show file tree
Hide file tree
Showing 13 changed files with 616 additions and 642 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"aegir": "^18.2.1",
"bitcoinjs-lib": "^4.0.2",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"dirty-chai": "^2.0.1",
"ethereumjs-block": "^2.1.0",
"ipfs-block-service": "~0.15.2",
Expand Down
156 changes: 50 additions & 106 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
const Block = require('ipfs-block')
const pull = require('pull-stream')
const CID = require('cids')
const doUntil = require('async/doUntil')
const joinPath = require('path').join
const osPathSep = require('path').sep
const pullDeferSource = require('pull-defer').source
const pullTraverse = require('pull-traverse')
const map = require('async/map')
Expand All @@ -14,6 +11,7 @@ const mergeOptions = require('merge-options')
const ipldDagCbor = require('ipld-dag-cbor')
const ipldDagPb = require('ipld-dag-pb')
const ipldRaw = require('ipld-raw')
const { fancyIterator } = require('./util')

function noop () {}

Expand Down Expand Up @@ -62,111 +60,76 @@ class IPLDResolver {
}
}

get (cid, path, options, callback) {
if (typeof path === 'function') {
callback = path
path = undefined
}

if (typeof options === 'function') {
callback = options
options = {}
/**
* Retrieves IPLD Nodes along the `path` that is rooted at `cid`.
*
* @param {CID} cid - the CID the resolving starts.
* @param {string} path - the path that should be resolved.
* @returns {Iterable.<Promise.<{remainderPath: string, value}>>} - Returns an async iterator of all the IPLD Nodes that were traversed during the path resolving. Every element is an object with these fields:
* - `remainderPath`: the part of the path that wasn’t resolved yet.
* - `value`: the value where the resolved path points to. If further traversing is possible, then the value is a CID object linking to another IPLD Node. If it was possible to fully resolve the path, value is the value the path points to. So if you need the CID of the IPLD Node you’re currently at, just take the value of the previously returned IPLD Node.
*/
resolve (cid, path) {
if (!CID.isCID(cid)) {
throw new Error('`cid` argument must be a CID')
}

// this removes occurrences of ./, //, ../
// makes sure that path never starts with ./ or /
// path.join is OS specific. Need to convert back to POSIX format.
if (typeof path === 'string') {
path = joinPath('/', path)
.substr(1)
.split(osPathSep)
.join('/')
if (typeof path !== 'string') {
throw new Error('`path` argument must be a string')
}

if (path === '' || !path) {
return this._get(cid, (err, node) => {
if (err) {
return callback(err)
}
callback(null, {
value: node,
remainderPath: '',
cid
})
})
}

let value
const next = () => {
// End iteration if there isn't a CID to follow anymore
if (cid === null) {
return Promise.resolve({ done: true })
}

doUntil(
(cb) => {
return new Promise((resolve, reject) => {
this._getFormat(cid.codec, (err, format) => {
if (err) return cb(err)
if (err) {
return reject(err)
}

// get block
// use local resolver
// update path value
this.bs.get(cid, (err, block) => {
if (err) {
return cb(err)
return reject(err)
}

format.resolver.resolve(block.data, path, (err, result) => {
if (err) {
return cb(err)
return reject(err)
}
value = result.value

// Prepare for the next iteration if there is a `remainderPath`
path = result.remainderPath
cb()
let value = result.value
// NOTE vmx 2018-11-29: Not all IPLD Formats return links as
// CIDs yet. Hence try to convert old style links to CIDs
if (Object.keys(value).length === 1 && '/' in value) {
value = new CID(value['/'])
}
if (CID.isCID(value)) {
cid = value
} else {
cid = null
}

return resolve({
done: false,
value: {
remainderPath: path,
value
}
})
})
})
})
},
() => {
const endReached = !path || path === '' || path === '/'
const isTerminal = value && !IPLDResolver._maybeCID(value)

if ((endReached && isTerminal) || options.localResolve) {
cid = IPLDResolver._maybeCID(value) || cid

return true
} else {
value = IPLDResolver._maybeCID(value)
// continue traversing
if (value) {
cid = value
}
return false
}
},
(err, results) => {
if (err) {
return callback(err)
}
return callback(null, {
value: value,
remainderPath: path,
cid
})
}
)
}

getStream (cid, path, options) {
const deferred = pullDeferSource()

this.get(cid, path, options, (err, result) => {
if (err) {
return deferred.resolve(
pull.error(err)
)
}
deferred.resolve(
pull.values([result])
)
})
})
}

return deferred
return fancyIterator(next)
}

/**
Expand Down Expand Up @@ -347,25 +310,6 @@ class IPLDResolver {
/* */
/* internals */
/* */

_get (cid, callback) {
waterfall([
(cb) => this._getFormat(cid.codec, cb),
(format, cb) => this.bs.get(cid, (err, block) => {
if (err) return cb(err)
cb(null, format, block)
}),
(format, block, cb) => {
format.util.deserialize(block.data, (err, deserialized) => {
if (err) {
return cb(err)
}
cb(null, deserialized)
})
}
], callback)
}

_getFormat (codec, callback) {
if (this.resolvers[codec]) {
return callback(null, this.resolvers[codec])
Expand Down
32 changes: 32 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict'

exports.first = async (iterator) => {
for await (const value of iterator) {
return value
}
}

exports.last = async (iterator) => {
let value
for await (value of iterator) {
// Intentionally empty
}
return value
}

exports.all = async (iterator) => {
const values = []
for await (const value of iterator) {
values.push(value)
}
return values
}

exports.fancyIterator = (next) => {
const iterator = { next }
iterator[Symbol.asyncIterator] = function () { return this }
iterator.first = () => exports.first(iterator)
iterator.last = () => exports.last(iterator)
iterator.all = () => exports.all(iterator)
return iterator
}
35 changes: 18 additions & 17 deletions test/basics.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

const chai = require('chai')
const dirtyChai = require('dirty-chai')
const chaiAsProised = require('chai-as-promised')
const expect = chai.expect
chai.use(dirtyChai)
chai.use(chaiAsProised)
const BlockService = require('ipfs-block-service')
const CID = require('cids')
const multihash = require('multihashes')
Expand Down Expand Up @@ -33,29 +35,28 @@ module.exports = (repo) => {
})

describe('validation', () => {
it('get - errors on unknown resolver', (done) => {
it('resolve - errors on unknown resolver', async () => {
const bs = new BlockService(repo)
const r = new IPLDResolver({ blockService: bs })
// choosing a format that is not supported
const cid = new CID(1, 'base1', multihash.encode(Buffer.from('abcd', 'hex'), 'sha1'))
r.get(cid, '/', {}, (err, result) => {
expect(err).to.exist()
expect(err.message).to.eql('No resolver found for codec "base1"')
done()
})
const result = r.resolve(cid, '')
await expect(result.next()).to.be.rejectedWith(
'No resolver found for codec "base1"')
})

it('_get - errors on unknown resolver', (done) => {
const bs = new BlockService(repo)
const r = new IPLDResolver({ blockService: bs })
// choosing a format that is not supported
const cid = new CID(1, 'base1', multihash.encode(Buffer.from('abcd', 'hex'), 'sha1'))
r.get(cid, (err, result) => {
expect(err).to.exist()
expect(err.message).to.eql('No resolver found for codec "base1"')
done()
})
})
// TODO vmx 2018-11-29 Change this test to use `get()`.
// it('_get - errors on unknown resolver', (done) => {
// const bs = new BlockService(repo)
// const r = new IPLDResolver({ blockService: bs })
// // choosing a format that is not supported
// const cid = new CID(1, 'base1', multihash.encode(Buffer.from('abcd', 'hex'), 'sha1'))
// r.get(cid, (err, result) => {
// expect(err).to.exist()
// expect(err.message).to.eql('No resolver found for codec "base1"')
// done()
// })
// }

it('put - errors on unknown resolver', (done) => {
const bs = new BlockService(repo)
Expand Down
40 changes: 16 additions & 24 deletions test/format-support.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

const chai = require('chai')
const dirtyChai = require('dirty-chai')
const chaiAsProised = require('chai-as-promised')
const expect = chai.expect
chai.use(dirtyChai)
chai.use(chaiAsProised)
const BlockService = require('ipfs-block-service')
const dagCBOR = require('ipld-dag-cbor')

Expand All @@ -28,21 +30,19 @@ module.exports = (repo) => {
})

describe('Dynamic format loading', () => {
it('should fail to dynamically load format', (done) => {
it('should fail to dynamically load format', async () => {
const bs = new BlockService(repo)
const resolver = new IPLDResolver({
blockService: bs,
formats: []
})

resolver.get(cid, '/', (err) => {
expect(err).to.exist()
expect(err.message).to.equal('No resolver found for codec "dag-cbor"')
done()
})
const result = resolver.resolve(cid, '')
await expect(result.next()).to.be.rejectedWith(
'No resolver found for codec "dag-cbor"')
})

it('should fail to dynamically load format via loadFormat option', (done) => {
it('should fail to dynamically load format via loadFormat option', async () => {
const errMsg = 'BOOM' + Date.now()
const bs = new BlockService(repo)
const resolver = new IPLDResolver({
Expand All @@ -54,14 +54,11 @@ module.exports = (repo) => {
}
})

resolver.get(cid, '/', (err) => {
expect(err).to.exist()
expect(err.message).to.equal(errMsg)
done()
})
const result = resolver.resolve(cid, '')
await expect(result.next()).to.be.rejectedWith(errMsg)
})

it('should dynamically load missing format', (done) => {
it('should dynamically load missing format', async () => {
const bs = new BlockService(repo)
const resolver = new IPLDResolver({
blockService: bs,
Expand All @@ -72,14 +69,12 @@ module.exports = (repo) => {
}
})

resolver.get(cid, '/', (err, result) => {
expect(err).to.not.exist()
expect(result.value).to.eql(data)
done()
})
const result = resolver.resolve(cid, '')
const node = await result.first()
expect(node.value).to.eql(data)
})

it('should not dynamically load format added statically', (done) => {
it('should not dynamically load format added statically', async () => {
const bs = new BlockService(repo)
const resolver = new IPLDResolver({
blockService: bs,
Expand All @@ -89,11 +84,8 @@ module.exports = (repo) => {
}
})

resolver.get(cid, '/', (err, result) => {
expect(err).to.not.exist()
expect(result.value).to.eql(data)
done()
})
const result = resolver.resolve(cid, '')
await result.next()
})
})
})
Expand Down

0 comments on commit 162473b

Please sign in to comment.