From a95e2c7f881f98ec320f8bdd1fc39c8086d4bb78 Mon Sep 17 00:00:00 2001 From: Tyler Hall Date: Wed, 30 Oct 2024 22:06:29 +0000 Subject: [PATCH] feat(parse-data-protocol): add utility that implements NAS-115 grouping convention #1059 --- parse-data-protocol/README.md | 285 +++++++++++++++ parse-data-protocol/esbuild.js | 25 ++ parse-data-protocol/index.js | 273 +++++++++++++++ parse-data-protocol/index.test.js | 258 ++++++++++++++ parse-data-protocol/package-lock.json | 479 ++++++++++++++++++++++++++ parse-data-protocol/package.json | 31 ++ parse-data-protocol/types.ts | 4 + 7 files changed, 1355 insertions(+) create mode 100644 parse-data-protocol/README.md create mode 100644 parse-data-protocol/esbuild.js create mode 100644 parse-data-protocol/index.js create mode 100644 parse-data-protocol/index.test.js create mode 100644 parse-data-protocol/package-lock.json create mode 100644 parse-data-protocol/package.json create mode 100644 parse-data-protocol/types.ts diff --git a/parse-data-protocol/README.md b/parse-data-protocol/README.md new file mode 100644 index 000000000..477fe9787 --- /dev/null +++ b/parse-data-protocol/README.md @@ -0,0 +1,285 @@ +# parse-data-protocol + +A set of utilities for extracting and parsing tags associated with ANS-115 +`Data-Protocol`s + + + +- [Why](#why) +- [Usage](#usage) + - [`findAll`](#findall) + - [`findAllByName`](#findallbyname) + - [`findByName`](#findbyname) + - [`parseAll`](#parseall) + - [`parse`](#parse) + - [`create`](#create) + - [`assoc`](#assoc) + - [`proto`](#proto) + + + +## Why + +**It is fundamental to note that, in ANS-104, tags are ordered.** + +ANS-115 specifies how to utilize `Data-Protocol`s to compose application level +specification using ANS-104 tags. + +However, ambiguity arises when a piece of data implements many `Data-Protocol` +which specify the same tag name: + +```js +const tags = [ + { name: "Data-Protocol", value: "ao" }, + // ... + { name: "Data-Protocol", value: "zone" }, + // Which tags goes with which Data-Protocol? + { name: "Type", value: "Process" }, + { name: "Type", value: "Profile" }, + { name: "Variant", value: "ao.TN.1" }, + { name: "Variant", value: "0.0.2" }, +]; +``` + +This ambuguity can lead to unexpected behavior in workarounds in subsequent +implementations. + +By enforcing one additional simple convention, this ambiguity is massively +curtailed: + +> tags are associated with the most recent `Data-Protocol` tag. **A corollary is** +> **that tags not belonging to a `Data-Protocol` would appear first, before any** +> **`Data-Protocol` tags.** + +```js +const tags = [ + { name: "Data-Protocol", value: "ao" }, + // these are associated with ao Data-Protocol + { name: "Type", value: "Process" }, + { name: "Variant", value: "ao.TN.1" }, + // end ao tags + { name: "Data-Protocol", value: "zone" }, + // these are asscociated with zone Data-Protocol + { name: "Type", value: "Profile" }, + { name: "Variant", value: "0.0.2" }, +]; +``` + +This module provides utilties for interacting with Data-Protocol tags, using +that convention. In this module, this convention is referred to as +"`association`" ie. tags are associated with the most recent `Data-Protocol` + +## Usage + +```js +import { + concat, + create, + findAll, + findAllByName, + findByName, + parse, + parseAll, + proto, +} from "@permaweb/parse-data-protocol"; + +const tags = [ + { name: "Data-Protocol", value: "ao" }, + // these are associated with ao Data-Protocol + { name: "Type", value: "Process" }, + { name: "Variant", value: "ao.TN.1" }, + // end ao tags + { name: "Data-Protocol", value: "zone" }, + // these are asscociated with zone Data-Protocol + { name: "Type", value: "Profile" }, + { name: "Variant", value: "0.0.2" }, + // end zone tags +]; + +// use a top-level export, passing protocol first +const aoType = findByName("ao", "Type", tags); + +// OR use the proto helper to get an API per Data-Protocol +const ao = proto("ao"); +const zone = proto("zone"); + +// No longer need to pass protocol first +const aoType = ao.value("Type", tags); +const zoneTypes = zone.values("Type", tags); +``` + +> Passing `protocol` first, over and over, might get verbose. Alternatively, you +> can use the [`proto`](#proto) helper. + +### `findAll` + +Extract the tags associated with the provided `Data-Protocol`. + +If the `Data-Protocol` tag is NOT found, then ALL tags are considered associated +with the `Data-Protocol`. + +```js +import { findAll } from "@permaweb/parse-data-protocol"; + +const tags = [/*...*/]; + +// [{ name, value }, ...] +const aoTags = findAll("ao", tags); +``` + +### `findAllByName` + +Extract the tags, with the name, associated with the provided `Data-Protocol`. + +```js +import { findAllByName } from '@permaweb/parse-data-protocol' + +const tags = [/*...*/] + +// [{ name, value }, ...] +const zoneTypes = findAllByName('zone', 'Type' tags) +``` + +### `findByName` + +Extract the FIRST tag, with the name, associated with the provided +`Data-Protocol`. + +```js +import { findByName } from '@permaweb/parse-data-protocol' + +const tags = [/*...*/] + +// { name, value } +const aoType = findAllByName('ao', 'Type' tags) +``` + +### `parseAll` + +Parse tags associated with the `Data-Protocol` into an object with key-value +pairs of name -> an array of values. + +At each key, the values in each array will be in order of appearance + +```js +import { parseAll } from "@permaweb/parse-data-protocol"; + +const tags = [/*...*/]; + +// { Type: ['Process', ...], Module: ['...'] } +const aoParsed = parseAll("ao", tags); +``` + +### `parse` + +Parse tags associated with the `Data-Protocol` into an object with key-value +pairs of name -> value. + +If multiple tags are found, then the FIRST tag value is used, and subsequent +values are discarded. If you'd like to preserve all values, then use +[`parseAll`](#parseall) + +```js +import { parse } from "@permaweb/parse-data-protocol"; + +const tags = [/*...*/]; + +// { Type: 'Process', Module: '...' } +const aoParsed = parse("ao", tags); +``` + +### `create` + +Associate an array of tags associated with the Data-Protocol. The +`Data-Protocol` tag will be prepended to the front of the array. + +```js +import { create } from "@permaweb/parse-data-protocol"; + +const pTags = [{ name: "Foo", value: "Bar" }]; + +/** +[ + { name: 'Data-Protocol', value: 'ao' }, + { name: 'Foo', value: 'Bar' } +] + */ +const aoTags = create("ao", pTags); +``` + +### `assoc` + +Produce a new array of tags by associating the provided tags with the +`Data-Protocol`. The new associations are appended to end of an existing +_subsection_. + +If there is no existing _subsection_ of tags for the `Data-Protocol`, then the +new section is appended to the end + +NO deduplication is performed on the associated tags. + +```js +import { assoc } from "@permaweb/parse-data-protocol"; + +const tags = [ + { name: "Data-Protocol", value: "ao" }, + // these are associated with ao Data-Protocol + { name: "Type", value: "Process" }, + { name: "Variant", value: "ao.TN.1" }, + // end ao tags + { name: "Data-Protocol", value: "zone" }, + // these are asscociated with zone Data-Protocol + { name: "Type", value: "Profile" }, + { name: "Variant", value: "0.0.2" }, + // end zone tags +]; + +const pTags = [{ name: "Foo", value: "Bar" }, { name: "Cool", value: "Beans" }]; + +// ao subsection is appended to +const newTags = assoc("ao", pTags, tags)[ + { name: "Data-Protocol", value: "ao" }, + // these are associated with ao Data-Protocol + { name: "Type", value: "Process" }, + { name: "Variant", value: "ao.TN.1" }, + { name: "Foo", value: "Bar" }, + { name: "Cool", value: "Beans" }, + // end ao tags + { name: "Data-Protocol", value: "zone" }, + // these are asscociated with zone Data-Protocol + { name: "Type", value: "Profile" }, + { name: "Variant", value: "0.0.2" } + // end zone tags +]; +``` + +### `proto` + +Instead of constantly passing `protocol` as the first argument every time, you +can use this api. + +Build a `@permaweb/parse-data-protocol` API for a single `Data-Protocol` + +```js +import { proto } from "@permaweb/parse-data-protocol"; + +const ao = proto("ao"); +const zone = proto("zone"); + +const tags = [ + { name: "Data-Protocol", value: "ao" }, + // these are associated with ao Data-Protocol + { name: "Type", value: "Process" }, + { name: "Variant", value: "ao.TN.1" }, + // end ao tags + { name: "Data-Protocol", value: "zone" }, + // these are asscociated with zone Data-Protocol + { name: "Type", value: "Profile" }, + { name: "Variant", value: "0.0.2" }, +]; + +// 'Process' +const aoType = ao.value("Type", tags); +// ['Profile'] +const zoneTypes = zone.values("Type", tags); +``` diff --git a/parse-data-protocol/esbuild.js b/parse-data-protocol/esbuild.js new file mode 100644 index 000000000..939f01d9a --- /dev/null +++ b/parse-data-protocol/esbuild.js @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs' +import * as esbuild from 'esbuild' + +/** + * By importing from manifest, build will always be in sync with the manifest + */ +const manifest = JSON.parse(readFileSync('./package.json')) + +// CJS +await esbuild.build({ + entryPoints: ['index.js'], + platform: 'node', + format: 'cjs', + bundle: true, + outfile: manifest.main +}) + +// ESM +await esbuild.build({ + entryPoints: ['index.js'], + platform: 'node', + format: 'esm', + bundle: true, + outfile: manifest.module +}) diff --git a/parse-data-protocol/index.js b/parse-data-protocol/index.js new file mode 100644 index 000000000..76c986900 --- /dev/null +++ b/parse-data-protocol/index.js @@ -0,0 +1,273 @@ +const pipe = (...fns) => (i) => + fns.reduce((acc, fn) => fn(acc), i) + +const defaultTo = (dVal) => (val) => val == null ? dVal : val + +const propOr = (defaultV) => (prop) => pipe( + (obj) => obj ? obj[prop] : obj, + defaultTo(defaultV) +) + +const mapObject = (fn) => (obj) => { + const res = {} + for (const key in obj) { + // eslint-disable-next-line + if (obj.hasOwnProperty(key)) res[key] = fn(obj[key], key, obj) + } + return res +} + +const complement = (fn) => (...args) => !fn(...args) + +/** + * @typedef {Object} Tag + * @property {string} name - The name of the tag + * @property {string} value - The value of the tag + */ + +const findProtocolBoundaries = (protocol) => (tags) => { + /** + * Find the start of the tags associated with the Data-Protocol. + */ + const startIdx = tags.findIndex(t => t.name === 'Data-Protocol' && t.value === protocol) + if (startIdx === -1) return [0, 0] + + /** + * There might be additional Data-Protocols after the one we're + * interested in, so find the end of the tags aka. "up to" a possible + * next Data-Protocol + */ + let endIdx = tags.findIndex((t, idx) => idx > startIdx && t.name === 'Data-Protocol' && t.value !== protocol) + if (endIdx === -1) endIdx = tags.length + + return [startIdx, endIdx] +} + +const byName = (name) => (t) => t.name === name + +/** + * @param {string} protocol - The Data-Protocol whose tags will be found + * @param {Tag[]} tags - An array of tags to filter + * @returns {Tag[]} - An array of tags associated with the Data-Protocol + */ +export const findAll = (protocol, tags) => pipe( + findProtocolBoundaries(protocol), + ([start, end]) => tags.slice(start, end) +)(tags) + +/** + * @param {string} protocol - The Data-Protocol + * @param {string} name - The tag name to search for + * @param {Tag[]} tags - An array of tags to filter + * @returns {Tag[]} - An array of tags associated with the specified Data-Protocol + */ +export const findAllByName = (protocol, name, tags) => pipe( + (tags) => findAll(protocol, tags), + (pTags) => pTags.filter(byName(name)) +)(tags) + +/** + * @param {string} protocol - The Data-Protocol + * @param {string} name - The tag name to search for + * @param {Tag[]} tags - An array of tags to filter + * @returns {Tag | undefined} - An array of tags associated with the specified Data-Protocol + */ +export const findByName = (protocol, name, tags) => pipe( + (tags) => findAllByName(protocol, name, tags), + (arr) => arr[0] +)(tags) + +/** + * @param {string} protocol - The Data-Protocol to associate with the tags + * @param {Tag[]} pTags - An array of tags to associate with the Data-Protocol + * @returns {Tag[]} - an array of tags, associated with the Data-Protocol tag, as the first item + */ +export const create = (protocol, pTags) => { + pTags = pTags.filter(t => t.name !== 'Data-Protocol' || t.value !== protocol) + + if (!pTags.length) return [] + + return [ + { name: 'Data-Protocol', value: protocol }, + ...pTags + ] +} + +/** + * @param {string} protocol - The Data-Protocol to associate with the tags + * @param {Tag[]} pTags - An array of tags to associate with the Data-Protocol + * @param {Tag[]} tags - The array of tags being concatenated to + * @returns {Tag[]} - an array of tags with protocol tags concatenated to the subsection + */ +export const concat = (protocol, pTags, tags) => { + const [start, end] = findProtocolBoundaries(protocol)(tags) + let [before, cur, after] = [ + tags.slice(0, start), + tags.slice(start, end), + tags.slice(end) + ] + + if (!cur.length) { + pTags = create(protocol, pTags) + before = after + after = [] + } + + return [before, cur, pTags, after].flat(1) +} + +/** + * @param {string} protocol - The Data-Protocol to associate with the tags + * @param {Tag[]} pTags - An array of tags to associate with the Data-Protocol + * @param {Tag[]} tags - The array of tags being concatenated to + * @returns {Tag[]} - an array of tags with protocol tags updated in the subsection + */ +export const update = (protocol, pTags, tags) => { + const [start, end] = findProtocolBoundaries(protocol)(tags) + let [before, after] = [tags.slice(0, start), tags.slice(end)] + + if (after.length === tags.length) { + before = after + after = [] + } + + return [before, create(protocol, pTags), after].flat(1) +} + +/** + * @param {string} protocol - The Data-Protocol to associate with the tags + * @param {Tag[]} tags - The array of tags being removed from + * @returns {Tag[]} - an array of tags with protocol tags removed + */ +export const removeAll = (protocol, tags) => update(protocol, [], tags) + +/** + * @param {string} protocol - The Data-Protocol to associate with the tags + * @param {string} name - The name of the tags to remove in the protocol + * @param {Tag[]} tags - The array of tags being removed from + * @returns {Tag[]} - an array of tags with protocol tags removed + */ +export const removeAllByName = (protocol, name, tags) => { + const [start, end] = findProtocolBoundaries(protocol)(tags) + + let [before, cur, after] = [tags.slice(0, start), tags.slice(start, end), tags.slice(end)] + + cur = cur.filter(complement(byName(name))) + + return [before, cur, after].flat(1) +} + +/** + * By default, only the value of a tag's first occurrence is used. + * + * Instead, if multi is set to true, then each key will contain an array + * of values, in order of occurrence. + */ +const parseTags = (tags, multi = false) => pipe( + defaultTo([]), + /** + * Mutation is okay here, since it's + * an internal data structure + */ + (tags) => tags.reduce( + (parsed, tag) => pipe( + // [value, value, ...] || [] + propOr([])(tag.name), + // [value] + (arr) => { arr.push(tag.value); return arr }, + // { [name]: [value, value, ...] } + (arr) => { parsed[tag.name] = arr; return parsed } + )(parsed), + {} + ), + mapObject((values) => multi ? values : values[0]) +)(tags) + +const parseProtocol = (protocol, tags, multi) => pipe( + defaultTo([]), + (tags) => findAll(protocol, tags), + (tags) => parseTags(tags, multi) +)(tags) + +/** + * @param {string} protocol - The Data-Protocol whose tags will be parsed + * @param {Tag[]} tags - An array of tags to filter + * @returns {Record} - The object with key-value pairs of name -> an array of values. + */ +export const parseAll = (protocol, tags) => parseProtocol(protocol, tags, true) + +/** + * @param {string} protocol - The Data-Protocol whose tags will be parsed + * @param {Tag[]} tags - An array of tags to filter + * @returns {Record} - The object with key-value pairs of name -> an array of values. + */ +export const parse = (protocol, tags) => parseProtocol(protocol, tags, false) + +/** + * @param {Tag[]} tags - An array of tags to filter + * @returns {Record} - The object with key-value pairs of name -> an array of values. + */ +export const parseOthers = (tags) => { + let idx = tags.findIndex(t => t.name === 'Data-Protocol') + if (idx === -1) idx = tags.length + return parseTags(tags.slice(0, idx), false) +} + +/** + * @param {Tag[]} tags - An array of tags to filter + * @returns {Record} - The object with key-value pairs of name -> an array of values. + */ +export const parseAllOthers = (tags) => { + let idx = tags.findIndex(t => t.name === 'Data-Protocol') + if (idx === -1) idx = tags.length + return parseTags(tags.slice(0, idx), true) +} + +/** + * @param {string} p - the protocol to scope all apis to + * @returns a parse-data-protocol api scoped to the protocol + */ +export const proto = (p) => ({ + /** + * @type {import('./types').RemoveFirstArg} + */ + findAll: (tags) => findAll(p, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + findAllByName: (name, tags) => findAllByName(p, name, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + findByName: (name, tags) => findByName(p, name, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + create: (tags) => create(p, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + concat: (pTags, tags) => concat(p, pTags, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + update: (pTags, tags) => update(p, pTags, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + removeAll: (tags) => removeAll(p, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + removeAllByName: (name, tags) => removeAllByName(p, name, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + parse: (tags) => parse(p, tags), + /** + * @type {import('./types').RemoveFirstArg} + */ + parseAll: (tags) => parseAll(p, tags), + parseOthers, + parseAllOthers +}) diff --git a/parse-data-protocol/index.test.js b/parse-data-protocol/index.test.js new file mode 100644 index 000000000..c1a8cff77 --- /dev/null +++ b/parse-data-protocol/index.test.js @@ -0,0 +1,258 @@ +import { describe, test } from 'node:test' +import * as assert from 'node:assert' + +import { + concat, + create, + findAll, + findAllByName, + findByName, + parse, + parseAll, + parseAllOthers, + parseOthers, + removeAll, + removeAllByName, + update +} from './index.js' + +describe('suite', () => { + const [AO, ZONE] = ['ao', 'zone'] + const AO_DP = { name: 'Data-Protocol', value: 'ao' } + const AO_TAGS = [ + AO_DP, + { name: 'Variant', value: 'ao.TN.1' }, + { name: 'Type', value: 'Process' } + ] + const ZONE_DP = { name: 'Data-Protocol', value: 'zone' } + const ZONE_TAGS = [ + ZONE_DP, + { name: 'Type', value: 'Profile' }, + { name: 'Type', value: 'What' }, + { name: 'Variant', value: '0.0.2' } + ] + + const RAND = { name: 'Random', value: 'Tag' } + const TAGS = [ + RAND, + AO_TAGS, + ZONE_TAGS + ].flat(1) + + describe('findAll', () => { + test('should find all protocol tags', () => { + assert.deepStrictEqual(findAll(AO, TAGS), AO_TAGS) + assert.deepStrictEqual(findAll(ZONE, TAGS), ZONE_TAGS) + }) + + test('should find no tags', () => { + assert.deepStrictEqual(findAll(AO, []), []) + assert.deepStrictEqual(findAll('foo', TAGS), []) + }) + }) + + describe('findAllByName', () => { + test('should find all protocol tags by name', () => { + assert.deepStrictEqual( + findAllByName(AO, 'Type', TAGS), + [{ name: 'Type', value: 'Process' }] + ) + + assert.deepStrictEqual( + findAllByName(ZONE, 'Type', TAGS), + [ + { name: 'Type', value: 'Profile' }, + { name: 'Type', value: 'What' } + ] + ) + }) + + test('should find no tags', () => { + assert.deepStrictEqual(findAllByName(AO, 'Foo', TAGS), []) + assert.deepStrictEqual(findAllByName(ZONE, 'Foo', TAGS), []) + }) + }) + + describe('findByName', () => { + test('should find the first protocol tag by name', () => { + assert.deepStrictEqual( + findByName(AO, 'Type', TAGS), + { name: 'Type', value: 'Process' } + ) + + assert.deepStrictEqual( + findByName(ZONE, 'Type', TAGS), + { name: 'Type', value: 'Profile' } + ) + }) + + test('should find no tag', () => { + assert.deepStrictEqual(findByName(AO, 'Foo', TAGS), undefined) + assert.deepStrictEqual(findByName(ZONE, 'Foo', TAGS), undefined) + }) + }) + + describe('create', () => { + test('should create protocol tags', () => { + assert.deepStrictEqual(create(AO, AO_TAGS.slice(1)), AO_TAGS) + // dedupe Data-Protocol tag + assert.deepStrictEqual(create(AO, AO_TAGS), AO_TAGS) + }) + + test('should create no tags', () => { + assert.deepStrictEqual(create(AO, []), []) + assert.deepStrictEqual(create(AO, AO_TAGS.slice(0, 1)), []) + }) + }) + + describe('concat', () => { + const NEW_TAGS = [ + { name: 'Type', value: 'Foo' }, + { name: 'Foo', value: 'Bar' } + ] + + test('should concatenate to the protocol tags', () => { + assert.deepStrictEqual( + concat(AO, NEW_TAGS, TAGS), + [RAND, AO_TAGS, NEW_TAGS, ZONE_TAGS].flat(1) + ) + + assert.deepStrictEqual( + concat(ZONE, NEW_TAGS, TAGS), + [RAND, AO_TAGS, ZONE_TAGS, NEW_TAGS].flat(1) + ) + }) + + test('should concatenate new protocol section to the end', () => { + assert.deepStrictEqual( + concat(AO, NEW_TAGS, ZONE_TAGS), + [ZONE_TAGS, AO_DP, NEW_TAGS].flat(1) + ) + }) + }) + + describe('update', () => { + const NEW_TAGS = [ + { name: 'Type', value: 'Foo' }, + { name: 'Foo', value: 'Bar' } + ] + + test('should replace the protocol tags, preserving order', () => { + assert.deepStrictEqual( + update(AO, NEW_TAGS, TAGS), + [RAND, AO_DP, NEW_TAGS, ZONE_TAGS].flat(1) + ) + + assert.deepStrictEqual( + update(ZONE, NEW_TAGS, TAGS), + [RAND, AO_TAGS, ZONE_DP, NEW_TAGS].flat(1) + ) + }) + + test('should concatenate new protocol section to the end', () => { + assert.deepStrictEqual( + update(AO, NEW_TAGS, ZONE_TAGS), + [ZONE_TAGS, AO_DP, NEW_TAGS].flat(1) + ) + }) + + test('should do nothing', () => { + assert.deepStrictEqual(update(AO, [], ZONE_TAGS), ZONE_TAGS) + assert.deepStrictEqual(update(AO, [], [RAND]), [RAND]) + assert.deepStrictEqual(update(AO, [], []), []) + }) + }) + + describe('removeAll', () => { + test('should remove all protocol tags', () => { + assert.deepStrictEqual(removeAll(AO, TAGS), [RAND, ZONE_TAGS].flat(1)) + assert.deepStrictEqual(removeAll(ZONE, TAGS), [RAND, AO_TAGS].flat(1)) + }) + + test('should do nothing', () => { + assert.deepStrictEqual( + removeAll(AO, [RAND, ZONE_TAGS].flat(1)), + [RAND, ZONE_TAGS].flat(1) + ) + + assert.deepStrictEqual(removeAll(AO, []), []) + }) + }) + + describe('removeAllByName', () => { + test('should remove all protocol tags, by name', () => { + assert.deepStrictEqual( + removeAllByName(AO, 'Type', TAGS), + [RAND, AO_DP, { name: 'Variant', value: 'ao.TN.1' }, ZONE_TAGS].flat(1) + ) + + assert.deepStrictEqual( + removeAllByName(ZONE, 'Type', TAGS), + [RAND, AO_TAGS, ZONE_DP, { name: 'Variant', value: '0.0.2' }].flat(1) + ) + }) + + test('should do nothing', () => { + assert.deepStrictEqual(removeAllByName(AO, 'Foo', TAGS), TAGS) + assert.deepStrictEqual(removeAllByName(AO, 'Random', TAGS), TAGS) + }) + }) + + describe('parse', () => { + test('should parse into an object, use first value in protocol', () => { + assert.deepStrictEqual( + parse(AO, TAGS), + { 'Data-Protocol': 'ao', Type: 'Process', Variant: 'ao.TN.1' } + ) + + assert.deepStrictEqual( + parse(ZONE, TAGS), + { 'Data-Protocol': 'zone', Type: 'Profile', Variant: '0.0.2' } + ) + }) + + test('should parse to an empty object', () => { + assert.deepStrictEqual(parse(AO, [RAND, ZONE_TAGS].flat(1)), {}) + assert.deepStrictEqual(parse(AO, []), {}) + }) + }) + + describe('parseAll', () => { + test('should parse into an object, using all values', () => { + assert.deepStrictEqual( + parseAll(AO, TAGS), + { 'Data-Protocol': ['ao'], Type: ['Process'], Variant: ['ao.TN.1'] } + ) + + assert.deepStrictEqual( + parseAll(ZONE, TAGS), + { 'Data-Protocol': ['zone'], Type: ['Profile', 'What'], Variant: ['0.0.2'] } + ) + }) + + test('should parse to an empty object', () => { + assert.deepStrictEqual(parseAll(AO, [RAND, ZONE_TAGS].flat(1)), {}) + assert.deepStrictEqual(parseAll(AO, []), {}) + }) + }) + + describe('parseOthers', () => { + test('should parse tags not associated with a protocol, using first value', () => { + assert.deepStrictEqual( + parseOthers([{ name: 'Random', value: 'First' }, TAGS].flat(1)), + { Random: 'First' } + ) + }) + }) + + describe('parseAllOthers', () => { + test('should parse tags not associated with a protocol', () => { + assert.deepStrictEqual(parseAllOthers(TAGS), { Random: ['Tag'] }) + + assert.deepStrictEqual( + parseAllOthers([{ name: 'Random', value: 'First' }, TAGS].flat(1)), + { Random: ['First', 'Tag'] } + ) + }) + }) +}) diff --git a/parse-data-protocol/package-lock.json b/parse-data-protocol/package-lock.json new file mode 100644 index 000000000..b56ddca64 --- /dev/null +++ b/parse-data-protocol/package-lock.json @@ -0,0 +1,479 @@ +{ + "name": "@permaweb/parse-data-protocol", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@permaweb/parse-data-protocol", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "esbuild": "^0.24.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/parse-data-protocol/package.json b/parse-data-protocol/package.json new file mode 100644 index 000000000..8fe788582 --- /dev/null +++ b/parse-data-protocol/package.json @@ -0,0 +1,31 @@ +{ + "name": "@permaweb/parse-data-protocol", + "version": "0.0.1", + "description": "A utility for extracting and parsing tags associated with ANS-115 Data-Protocols", + "repository": { + "type": "git", + "url": "https://github.com/permaweb/ao.git", + "directory": "parse-data-protocol" + }, + "license": "MIT", + "author": "Tyler Hall", + "sideEffects": false, + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "browser": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "./dist" + ], + "scripts": { + "build": "npm run build:src && npm run build:types", + "build:src": "node esbuild.js", + "build:types": "tsc index.js --skipLibCheck --allowJS --declaration --emitDeclarationOnly --outDir dist", + "test": "node --test" + }, + "devDependencies": { + "esbuild": "^0.24.0", + "typescript": "^5.6.3" + } +} diff --git a/parse-data-protocol/types.ts b/parse-data-protocol/types.ts new file mode 100644 index 000000000..607211a14 --- /dev/null +++ b/parse-data-protocol/types.ts @@ -0,0 +1,4 @@ +export type RemoveFirstArg any> = + T extends (first: any, ...args: infer R) => infer RType + ? (...args: R) => RType + : never \ No newline at end of file