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

feat: integrate flow type-checker #82

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- npx aegir commitlint --travis
- npx aegir dep-check
- npm run lint
- npm run check

- stage: test
name: chrome
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"main": "src/index.js",
"scripts": {
"lint": "aegir lint",
"check": "flow check --color=always",
"test": "aegir test",
"test:node": "aegir test --target node",
"test:browser": "aegir test --target browser",
Expand All @@ -18,6 +19,7 @@
},
"pre-push": [
"lint",
"check",
"test"
],
"repository": {
Expand All @@ -41,6 +43,7 @@
},
"devDependencies": {
"aegir": "^18.2.0",
"flow-bin": "0.96.0",
"chai": "^4.2.0",
"dirty-chai": "^2.0.1",
"multihashing": "~0.3.3",
Expand Down
6 changes: 4 additions & 2 deletions src/cid-util.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow strict

'use strict'

const mh = require('multihashes')
Expand All @@ -9,9 +11,9 @@ var CIDUtil = {
* Returns undefined if it is a valid CID.
*
* @param {any} other
* @returns {string}
* @returns {?string}
*/
checkCIDComponents: function (other) {
checkCIDComponents: function (other /*: any */)/*: ?string */ {
if (other == null) {
return 'null values are not valid CIDs'
}
Expand Down
119 changes: 97 additions & 22 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow strict

'use strict'

const mh = require('multihashes')
Expand All @@ -7,11 +9,24 @@ const codecs = require('multicodec/src/base-table')
const CIDUtil = require('./cid-util')
const withIs = require('class-is')

/*::
export type Version = 0 | 1
export type Codec = string
export type Multihash = Buffer
export type BaseEncodedString = string
export type MultibaseName = string
export type SerializedCID = {
codec:Codec;
version:Version;
hash:Multihash;
}
*/

/**
* @typedef {Object} SerializedCID
* @param {string} codec
* @param {number} version
* @param {Buffer} multihash
* @param {Buffer} hash
*/

/**
Expand All @@ -28,7 +43,19 @@ const withIs = require('class-is')
* , as defined in [ipld/cid](https://github.com/multiformats/cid).
* @class CID
*/
class CID {
// CID<a> is generic and type parameter `a` represents type of the data that
// the CID addresses. While type parameter is not really used in this library
// it is still useful in enabling other libraries to encode type information
// e.g. it is possible to define functions like:
// function get <a>(CID<a>):Promise<a>
// function put <a>(a):Promise<CID<a>>
// Which would allow type-checker to infer type of the following value:
// const cid = await put({ x: 1, y: 2 })
// ...
// const point = await get(cid)
// point.x //:number
// point.z //:Cannot get `point.z` because property `z` is missing in object literal { x: 1, y: 2 }.
class CID /*:: <a> */{
/**
* Create a new CID.
*
Expand Down Expand Up @@ -59,10 +86,23 @@ class CID {
* new CID(<bs58 encoded multihash>)
* new CID(<cid>)
*/
constructor (version, codec, multihash, multibaseName = 'base58btc') {
if (module.exports.isCID(version)) {
// version is an exising CID instance
const cid = version
/*::
+version:Version
+codec:Codec
+multihash:Multihash
+multibaseName:MultibaseName
string:?string
_buffer:?Buffer
static isCID:(mixed) => boolean
*/
constructor (
version/*: CID<a> | BaseEncodedString | Buffer | Version */,
codec/*: ?Codec */ = undefined,
multihash/*: ?Multihash */ = undefined,
multibaseName/*: MultibaseName */ = 'base58btc'
) {
const cid = CID.matchCID(version)
if (cid) {
this.version = cid.version
this.codec = cid.codec
this.multihash = Buffer.from(cid.multihash)
Expand All @@ -76,7 +116,11 @@ class CID {
if (baseName) {
// version is a CID String encoded with multibase, so v1
const cid = multibase.decode(version)
this.version = parseInt(cid.slice(0, 1).toString('hex'), 16)
// Type checker will fail because parseInt isn't guaranteed to return
// 0 or 1 it can be any int. Invariant is later enforced through
// `validateCID` and we use any type to make type-checker trust us.
const v/*: any */ = parseInt(cid.slice(0, 1).toString('hex'), 16)
this.version = v
this.codec = multicodec.getCodec(cid.slice(1))
this.multihash = multicodec.rmPrefix(cid.slice(1))
this.multibaseName = baseName
Expand All @@ -92,7 +136,10 @@ class CID {
return
}

if (Buffer.isBuffer(version)) {
// type checker can refine type from predicate function like `isBuffer` but
// it can on instanceof check, which is why we use inline comment to enable
// that refinement.
if (Buffer.isBuffer(version) /*:: && version instanceof Buffer */) {
const firstByte = version.slice(0, 1)
const v = parseInt(firstByte.toString('hex'), 16)
if (v === 0 || v === 1) {
Expand All @@ -114,21 +161,26 @@ class CID {
}

// otherwise, assemble the CID from the parameters
// type checker will not accept `version`, `codec`, `multihash` as is
// because types don't correspond to [number, string, Buffer] so we
// use any below as we know `CID.validateCID` will throw if types do not
// match up.
const [$version, $codec, $multihash]/*: any */ = [version, codec, multihash]

/**
* @type {number}
*/
this.version = version
this.version = $version

/**
* @type {string}
*/
this.codec = codec
this.codec = $codec

/**
* @type {Buffer}
*/
this.multihash = multihash
this.multihash = $multihash

/**
* @type {string}
Expand All @@ -146,7 +198,7 @@ class CID {
*
* @memberOf CID
*/
get buffer () {
get buffer ()/*: Buffer */ {
let buffer = this._buffer

if (!buffer) {
Expand Down Expand Up @@ -175,7 +227,7 @@ class CID {
* @returns {Buffer}
* @readonly
*/
get prefix () {
get prefix ()/*: Buffer */ {
return Buffer.concat([
Buffer.from(`0${this.version}`, 'hex'),
multicodec.getCodeVarint(this.codec),
Expand All @@ -188,7 +240,7 @@ class CID {
*
* @returns {CID}
*/
toV0 () {
toV0 ()/*: CID<a> */ {
if (this.codec !== 'dag-pb') {
throw new Error('Cannot convert a non dag-pb CID to CIDv0')
}
Expand All @@ -211,7 +263,7 @@ class CID {
*
* @returns {CID}
*/
toV1 () {
toV1 ()/*: CID<a> */ {
return new _CID(1, this.codec, this.multihash)
}

Expand All @@ -221,7 +273,7 @@ class CID {
* @param {string} [base=this.multibaseName] - Base encoding to use.
* @returns {string}
*/
toBaseEncodedString (base = this.multibaseName) {
toBaseEncodedString (base/*: MultibaseName */ = this.multibaseName)/*: BaseEncodedString */ {
if (this.string && base === this.multibaseName) {
return this.string
}
Expand All @@ -243,7 +295,7 @@ class CID {
return str
}

toString (base) {
toString (base)/*: BaseEncodedString */ {
return this.toBaseEncodedString(base)
}

Expand All @@ -252,7 +304,7 @@ class CID {
*
* @returns {SerializedCID}
*/
toJSON () {
toJSON ()/*: SerializedCID */ {
return {
codec: this.codec,
version: this.version,
Expand All @@ -263,13 +315,18 @@ class CID {
/**
* Compare equality with another CID.
*
* @param {CID} other
* @param {any} other
* @returns {bool}
*/
equals (other) {
return this.codec === other.codec &&
equals (other/*: mixed */)/*: boolean */ {
return (
other != null &&
typeof other === 'object' &&
this.codec === other.codec &&
this.version === other.version &&
other.multihash instanceof Buffer &&
this.multihash.equals(other.multihash)
)
}

/**
Expand All @@ -279,12 +336,30 @@ class CID {
* @param {any} other
* @returns {void}
*/
static validateCID (other) {
static validateCID (other/*: mixed */)/*: void */ {
let errorMsg = CIDUtil.checkCIDComponents(other)
if (errorMsg) {
throw new Error(errorMsg)
}
}

/**
* Test if the given value is a valid CID object, if it is returns it back,
* otherwise returns undefined.
* @param {any} value
* @returns {?CID}
*/
static matchCID (value/*: mixed */)/*: ?CID<a> */ {
if (module.exports.isCID(value)) {
// Type checker is unable to refine type through predicate function,
// but we know value is CID so we use any making type-checker trust
// our judgment.
const cid/*: any */ = value
return cid
} else {
return undefined
}
}
}

const _CID = withIs(CID, {
Expand Down
33 changes: 0 additions & 33 deletions src/index.js.flow

This file was deleted.

32 changes: 32 additions & 0 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ describe('CID', () => {
it('.equals v0 to v0', () => {
expect(new CID(h1).equals(new CID(h1))).to.equal(true)
expect(new CID(h1).equals(new CID(h2))).to.equal(false)
expect(new CID(h1).equals(h1)).to.equal(false)
expect(new CID(h1).equals({ string: h1 })).to.equal(false)
})

it('.equals v0 to v1 and vice versa', () => {
Expand Down Expand Up @@ -245,6 +247,36 @@ describe('CID', () => {
CID.isCID(new CID(h1).toV1())
).to.equal(true)
})

it('.matchCID', () => {
const c1 = new CID(h1)

expect(
CID.matchCID(c1)
).to.equal(c1)

expect(
CID.matchCID(h1)
).to.equal(undefined)

expect(
CID.matchCID(false)
).to.equal(undefined)

expect(
CID.matchCID(Buffer.from('hello world'))
).to.equal(undefined)

const c1v0 = new CID(h1).toV0()
expect(
CID.matchCID(c1v0)
).to.equal(c1v0)

const c1v1 = new CID(h1).toV1()
expect(
CID.matchCID(c1v1)
).to.equal(c1v1)
})
})

describe('throws on invalid inputs', () => {
Expand Down