Skip to content

Commit

Permalink
Merge pull request #1 from RangerMauve/initial
Browse files Browse the repository at this point in the history
Initial version with tests
  • Loading branch information
RangerMauve authored Sep 20, 2022
2 parents c3b98d8 + c1cac6d commit 5ea5f3b
Show file tree
Hide file tree
Showing 4 changed files with 426 additions and 0 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,46 @@
# js-ipld-url-resolve
Resolver for IPLD URLs based on the js-IPFS DAG API. supports advanced features like schemas and escaping

## Features:

- Traverse IPLD URLs
- Support special character escaping in path segments (e.g. '/')
- Support for IPLD URL path segment parameter syntax `using ;`
- Support IPLD Schemas as lenses during traversal
- Resolve Links during traversal of schemas
- [x] Struct fields
- [x] Map values
- [x] List values
- [ ] Union types
- [ ] Links deeply nested within structs/maps
- ADL Registry for `schema` parameter to convert nodes

## API

```javascript
import IPLDURLSystem from 'js-ipld-url-resolve'

// You map provide an optional map of ADLs to use
const adls = new Map()

// This ADL will asynchronously stringify any data into a JSON string
// It's kinda useless, but you can make ADLs that return any JS object
// Whose properties can be getters that reuturn Promises that will get
// Automatically awaited during traversal.
// The `parameters` are the IPLDURL parameters for that node's segment
// Parameters might also be coming from the querystring if it's the root
adls.set('example', async (node, parameters, system) => JSON.stringify(node))

async function getNode(cid) {
const {value} = ipfs.dag.get(cid)
return value
}

const system = new IPLDURLSystem({
getNode,
adls
})

// Resolve some data from an IPLD URL
const data = await system.resolve('ipld://some_cid/some_path;schema=schema_cid;type=SchemaTypeName/plainpath/?adl=example')
```
163 changes: 163 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { IPLDURL } from 'js-ipld-url'
import { create as createTyped } from '@ipld/schema/typed.js'
import { toDSL } from '@ipld/schema/to-dsl.js'
import { CID } from 'multiformats/cid'
import printify from '@ipld/printify'
import { base32 } from 'multiformats/bases/base32'
import { base36 } from 'multiformats/bases/base36'

export const DEFAULT_CID_BASES = base32.decoder.or(base36.decoder)

export default class IPLDURLSystem {
constructor ({
getNode,
adls = new Map(),
cidBases = DEFAULT_CID_BASES
}) {
if (!getNode) throw new TypeError('Must provide a getNode function')
this.getNode = getNode
this.adls = adls
this.cidBases = cidBases
}

async resolve (url) {
const { hostname: root, segments, searchParams } = new IPLDURL(url)

const cid = CID.parse(root, this.cidBases).toV1()
let data = await this.getNode(cid)

const initialParameters = {}
let shouldProcessRoot = false
if (searchParams.has('schema')) {
shouldProcessRoot = true
initialParameters.schema = searchParams.get('schema')
initialParameters.type = searchParams.get('type')
}
if (searchParams.has('adl')) {
// TODO Should other parameters be passed to the ADL function?
shouldProcessRoot = true
initialParameters.adl = searchParams.get('adl')
}
if (shouldProcessRoot) {
data = await this.#applyParameters(data, initialParameters)
}

for (const { name, parameters } of segments) {
// This does enables ADLs to return promises for properties
data = await data[name]
const asCID = CID.asCID(data)
if (asCID) {
data = await this.getNode(asCID)
}
data = await this.#applyParameters(data, parameters)
}

return data
}

async #applyParameters (origin, { schema, adl, ...parameters }) {
let data = origin

const asCID = CID.asCID(data)
if (asCID) {
data = await this.getNode(asCID)
}

if (schema) {
data = await SchemaADL(data, { schema, ...parameters }, this)
}

if (adl) {
if (!this.adls.has(adl)) {
const known = [...this.adls.keys()].join(', ')
throw new Error(`Unknown ADL type ${adl}. Must be one of ${known}`)
}
data = await this.adls.get(adl)(data, parameters, this)
}

return data
}
}

export async function SchemaADL (node, { schema, type }, system) {
if (!schema || !type) {
throw new TypeError('Must specify which type to use with the schema parameter')
}

const schemaCID = CID.parse(schema, system.cidBases)
const schemaDMT = await system.getNode(schemaCID)
const converted = makeTyped(node, schemaDMT, type, system)

return converted
}

function makeTyped (node, schemaDMT, type, system) {
const typedSchema = createTyped(schemaDMT, type)
const converted = typedSchema.toTyped(node)

if (!converted) {
const dataView = printify(node)
const schemaDSL = toDSL(schemaDMT)
throw new Error(`Data did not match schema\nData: ${dataView}\nSchema: ${schemaDSL}`)
}

const typeDMT = schemaDMT.types[type]

// TODO: Account for union types and deeply nested links in structs
if (typeDMT.struct) {
const trapped = new Proxy(converted, {
get (target, property) {
const value = target[property]
const propertySchema = typeDMT.struct.fields[property]
const expectedType = propertySchema?.type?.link?.expectedType
if (!expectedType) return value
const asCID = CID.asCID(value)
if (!asCID) return value
return system.getNode(asCID).then((resolved) => {
return makeTyped(resolved, schemaDMT, expectedType, system)
})
}
})
return trapped
} else if (typeDMT.map) {
let valueType = typeDMT.map.valueType
if (typeof valueType === 'string') {
valueType = schemaDMT.types[valueType]
}
if (valueType?.link?.expectedType) {
const expectedType = valueType.link.expectedType
const trapped = new Proxy(converted, {
get (target, property) {
const value = target[property]
const asCID = CID.asCID(value)
if (!asCID) return value
return system.getNode(asCID).then((resolved) => {
return makeTyped(resolved, schemaDMT, expectedType, system)
})
}
})
return trapped
}
} else if (typeDMT.list) {
let valueType = typeDMT.map.valueType
if (typeof valueType === 'string') {
valueType = schemaDMT.types[valueType]
}
if (valueType?.link?.expectedType) {
const expectedType = valueType.link.expectedType
const trapped = new Proxy(converted, {
get (target, property) {
const value = target[property]
const asCID = CID.asCID(value)
if (!asCID) return value
return system.getNode(asCID).then((resolved) => {
return makeTyped(resolved, schemaDMT, expectedType, system)
})
}
})
return trapped
}
}

return converted
}
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "js-ipld-url-resolve",
"version": "1.0.0",
"description": "Resolver for IPLD URLs based on the js-IPFS DAG API. supports advanced features like schemas and escaping",
"main": "index.js",
"type": "module",
"scripts": {
"test": "node test.js",
"lint": "standard --fix"
},
"repository": {
"type": "git",
"url": "git+https://github.com/RangerMauve/js-ipld-url-resolve.git"
},
"keywords": [
"ipld",
"url",
"resolve"
],
"author": "rangermauve <ranger@mauve.moe> (https://mauve.moe/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/RangerMauve/js-ipld-url-resolve/issues"
},
"homepage": "https://github.com/RangerMauve/js-ipld-url-resolve#readme",
"dependencies": {
"@ipld/printify": "^0.1.0",
"@ipld/schema": "^4.1.0",
"js-ipld-url": "^1.0.2",
"multiformats": "^9.7.1"
},
"devDependencies": {
"ipfs-core": "^0.16.0",
"standard": "^17.0.0",
"tape": "^5.6.0"
}
}
Loading

0 comments on commit 5ea5f3b

Please sign in to comment.