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 4 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
2 changes: 2 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 Down Expand Up @@ -41,6 +42,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
Gozala marked this conversation as resolved.
Show resolved Hide resolved
*/

/**
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> */{
Gozala marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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'
) {
Gozala marked this conversation as resolved.
Show resolved Hide resolved
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]
Gozala marked this conversation as resolved.
Show resolved Hide resolved

/**
* @type {number}
*/
this.version = version
this.version = $version
Gozala marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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