From ae25ad9da9bf265d45a813db83935cc2b06f3f95 Mon Sep 17 00:00:00 2001 From: Hugo C <911307+hugocaillard@users.noreply.github.com> Date: Wed, 20 Sep 2023 19:50:31 +0200 Subject: [PATCH] feat: add Cl.prettyPrint (#1551) * feat: add pretty print * docs: improve Cl.prettyPrint docs * refactor: review --- packages/transactions/src/cl.ts | 2 + .../transactions/src/clarity/prettyPrint.ts | 128 +++++++++++++++++ .../transactions/tests/prettyPrint.test.ts | 136 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 packages/transactions/src/clarity/prettyPrint.ts create mode 100644 packages/transactions/tests/prettyPrint.test.ts diff --git a/packages/transactions/src/cl.ts b/packages/transactions/src/cl.ts index 1bf23b556..d06996f31 100644 --- a/packages/transactions/src/cl.ts +++ b/packages/transactions/src/cl.ts @@ -18,6 +18,8 @@ import { uintCV, } from './clarity'; +export { prettyPrint } from './clarity/prettyPrint'; + // todo: https://github.com/hirosystems/clarinet/issues/786 // Primitives ////////////////////////////////////////////////////////////////// diff --git a/packages/transactions/src/clarity/prettyPrint.ts b/packages/transactions/src/clarity/prettyPrint.ts new file mode 100644 index 000000000..b3f51f5dc --- /dev/null +++ b/packages/transactions/src/clarity/prettyPrint.ts @@ -0,0 +1,128 @@ +/* + Format Clarity Values into Clarity style readable strings + eg: + `Cl.uint(1)` => u1 + `Cl.list(Cl.uint(1))` => (list u1) + `Cl.tuple({ id: u1 })` => { id: u1 } +*/ + +import { bytesToHex } from '@stacks/common'; +import { ClarityType, ClarityValue, ListCV, TupleCV, principalToString } from '.'; + +function formatSpace(space: number, depth: number, end = false) { + if (!space) return ' '; + return `\n${' '.repeat(space * (depth - (end ? 1 : 0)))}`; +} + +/** + * @description format List clarity values in clarity style strings + * with the ability to prettify the result with line break end space indentation + * @example + * ```ts + * formatList(Cl.list([Cl.uint(1)])) + * // (list u1) + * + * formatList(Cl.list([Cl.uint(1)]), 2) + * // (list + * // u1 + * // ) + * ``` + */ +function formatList(cv: ListCV, space: number, depth = 1): string { + if (cv.list.length === 0) return '(list)'; + + const spaceBefore = formatSpace(space, depth, false); + const endSpace = space ? formatSpace(space, depth, true) : ''; + + const items = cv.list.map(v => prettyPrintWithDepth(v, space, depth)).join(spaceBefore); + + return `(list${spaceBefore}${items}${endSpace})`; +} + +/** + * @description format Tuple clarity values in clarity style strings + * with the ability to prettify the result with line break end space indentation + * @example + * ```ts + * formatTuple(Cl.tuple({ id: Cl.uint(1) })) + * // { id: u1 } + * + * formatTuple(Cl.tuple({ id: Cl.uint(1) }, 2)) + * // { + * // id: u1 + * // } + * ``` + */ +function formatTuple(cv: TupleCV, space: number, depth = 1): string { + if (Object.keys(cv.data).length === 0) return '{}'; + + const items: string[] = []; + for (const [key, value] of Object.entries(cv.data)) { + items.push(`${key}: ${prettyPrintWithDepth(value, space, depth)}`); + } + + const spaceBefore = formatSpace(space, depth, false); + const endSpace = formatSpace(space, depth, true); + + return `{${spaceBefore}${items.join(`,${spaceBefore}`)}${endSpace}}`; +} + +function exhaustiveCheck(param: never): never { + throw new Error(`invalid clarity value type: ${param}`); +} + +// the exported function should not expose the `depth` argument +function prettyPrintWithDepth(cv: ClarityValue, space = 0, depth: number): string { + if (cv.type === ClarityType.BoolFalse) return 'false'; + if (cv.type === ClarityType.BoolTrue) return 'true'; + + if (cv.type === ClarityType.Int) return cv.value.toString(); + if (cv.type === ClarityType.UInt) return `u${cv.value.toString()}`; + + if (cv.type === ClarityType.StringASCII) return `"${cv.data}"`; + if (cv.type === ClarityType.StringUTF8) return `u"${cv.data}"`; + + if (cv.type === ClarityType.PrincipalContract) return `'${principalToString(cv)}`; + if (cv.type === ClarityType.PrincipalStandard) return `'${principalToString(cv)}`; + + if (cv.type === ClarityType.Buffer) return `0x${bytesToHex(cv.buffer)}`; + + if (cv.type === ClarityType.OptionalNone) return 'none'; + if (cv.type === ClarityType.OptionalSome) + return `(some ${prettyPrintWithDepth(cv.value, space, depth)})`; + + if (cv.type === ClarityType.ResponseOk) + return `(ok ${prettyPrintWithDepth(cv.value, space, depth)})`; + if (cv.type === ClarityType.ResponseErr) + return `(err ${prettyPrintWithDepth(cv.value, space, depth)})`; + + if (cv.type === ClarityType.List) { + return formatList(cv, space, depth + 1); + } + if (cv.type === ClarityType.Tuple) { + return formatTuple(cv, space, depth + 1); + } + + // make sure that we exhausted all ClarityTypes + exhaustiveCheck(cv); +} + +/** + * @description format clarity values in clarity style strings + * with the ability to prettify the result with line break end space indentation + * @param cv The Clarity Value to format + * @param space The indentation size of the output string. There's no indentation and no line breaks if space = 0 + * @example + * ```ts + * prettyPrint(Cl.tuple({ id: Cl.some(Cl.uint(1)) })) + * // { id: (some u1) } + * + * prettyPrint(Cl.tuple({ id: Cl.uint(1) }, 2)) + * // { + * // id: u1 + * // } + * ``` + */ +export function prettyPrint(cv: ClarityValue, space = 0): string { + return prettyPrintWithDepth(cv, space, 0); +} diff --git a/packages/transactions/tests/prettyPrint.test.ts b/packages/transactions/tests/prettyPrint.test.ts new file mode 100644 index 000000000..1b5d704d0 --- /dev/null +++ b/packages/transactions/tests/prettyPrint.test.ts @@ -0,0 +1,136 @@ +import { Cl } from '../src'; + +describe.only('test format of Stacks.js clarity values into clarity style strings', () => { + it('formats basic types', () => { + expect(Cl.prettyPrint(Cl.bool(true))).toStrictEqual('true'); + expect(Cl.prettyPrint(Cl.bool(false))).toStrictEqual('false'); + expect(Cl.prettyPrint(Cl.none())).toStrictEqual('none'); + + expect(Cl.prettyPrint(Cl.int(1))).toStrictEqual('1'); + expect(Cl.prettyPrint(Cl.int(10n))).toStrictEqual('10'); + + expect(Cl.prettyPrint(Cl.stringAscii('hello world!'))).toStrictEqual('"hello world!"'); + expect(Cl.prettyPrint(Cl.stringUtf8('hello world!'))).toStrictEqual('u"hello world!"'); + }); + + it('formats principal', () => { + const addr = 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG'; + + expect(Cl.prettyPrint(Cl.standardPrincipal(addr))).toStrictEqual( + "'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG" + ); + expect(Cl.prettyPrint(Cl.contractPrincipal(addr, 'contract'))).toStrictEqual( + "'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG.contract" + ); + }); + + it('formats optional some', () => { + expect(Cl.prettyPrint(Cl.some(Cl.uint(1)))).toStrictEqual('(some u1)'); + expect(Cl.prettyPrint(Cl.some(Cl.stringAscii('btc')))).toStrictEqual('(some "btc")'); + expect(Cl.prettyPrint(Cl.some(Cl.stringUtf8('stx 🚀')))).toStrictEqual('(some u"stx 🚀")'); + }); + + it('formats reponse', () => { + expect(Cl.prettyPrint(Cl.ok(Cl.uint(1)))).toStrictEqual('(ok u1)'); + expect(Cl.prettyPrint(Cl.error(Cl.uint(1)))).toStrictEqual('(err u1)'); + expect(Cl.prettyPrint(Cl.ok(Cl.some(Cl.uint(1))))).toStrictEqual('(ok (some u1))'); + expect(Cl.prettyPrint(Cl.ok(Cl.none()))).toStrictEqual('(ok none)'); + }); + + it('formats buffer', () => { + expect(Cl.prettyPrint(Cl.buffer(Uint8Array.from([98, 116, 99])))).toStrictEqual('0x627463'); + expect(Cl.prettyPrint(Cl.bufferFromAscii('stx'))).toStrictEqual('0x737478'); + }); + + it('formats lists', () => { + expect(Cl.prettyPrint(Cl.list([1, 2, 3].map(Cl.int)))).toStrictEqual('(list 1 2 3)'); + expect(Cl.prettyPrint(Cl.list([1, 2, 3].map(Cl.uint)))).toStrictEqual('(list u1 u2 u3)'); + expect(Cl.prettyPrint(Cl.list(['a', 'b', 'c'].map(Cl.stringUtf8)))).toStrictEqual( + '(list u"a" u"b" u"c")' + ); + + expect(Cl.prettyPrint(Cl.list([]))).toStrictEqual('(list)'); + }); + + it('can prettify lists on multiple lines', () => { + const list = Cl.list([1, 2, 3].map(Cl.int)); + expect(Cl.prettyPrint(list)).toStrictEqual('(list 1 2 3)'); + expect(Cl.prettyPrint(list, 2)).toStrictEqual('(list\n 1\n 2\n 3\n)'); + + expect(Cl.prettyPrint(Cl.list([]), 2)).toStrictEqual('(list)'); + }); + + it('formats tuples', () => { + expect(Cl.prettyPrint(Cl.tuple({ counter: Cl.uint(10) }))).toStrictEqual('{ counter: u10 }'); + expect( + Cl.prettyPrint(Cl.tuple({ counter: Cl.uint(10), state: Cl.ok(Cl.stringUtf8('valid')) })) + ).toStrictEqual('{ counter: u10, state: (ok u"valid") }'); + + expect(Cl.prettyPrint(Cl.tuple({}))).toStrictEqual('{}'); + }); + + it('can prettify tuples on multiple lines', () => { + const tuple = Cl.tuple({ counter: Cl.uint(10) }); + + expect(Cl.prettyPrint(tuple)).toStrictEqual('{ counter: u10 }'); + expect(Cl.prettyPrint(tuple, 2)).toStrictEqual('{\n counter: u10\n}'); + + expect(Cl.prettyPrint(Cl.tuple({}), 2)).toStrictEqual('{}'); + }); + + it('prettifies nested list and tuples', () => { + // test that the right indentation level is applied for nested composite types + const addr = 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG'; + const value = Cl.tuple({ + id: Cl.uint(1), + messageAscii: Cl.stringAscii('hello world'), + someMessageUtf8: Cl.some(Cl.stringUtf8('hello world')), + items: Cl.some( + Cl.list([ + Cl.ok( + Cl.tuple({ + id: Cl.uint(1), + owner: Cl.some(Cl.standardPrincipal(addr)), + valid: Cl.ok(Cl.uint(2)), + history: Cl.some(Cl.list([Cl.uint(1), Cl.uint(2)])), + }) + ), + Cl.ok( + Cl.tuple({ + id: Cl.uint(2), + owner: Cl.none(), + valid: Cl.error(Cl.uint(1000)), + history: Cl.none(), + }) + ), + ]) + ), + }); + + const expected = `{ + id: u1, + messageAscii: "hello world", + someMessageUtf8: (some u"hello world"), + items: (some (list + (ok { + id: u1, + owner: (some 'ST2CY5V39NHDPWSXMW9QDT3HC3GD6Q6XX4CFRK9AG), + valid: (ok u2), + history: (some (list + u1 + u2 + )) + }) + (ok { + id: u2, + owner: none, + valid: (err u1000), + history: none + }) + )) +}`; + + const result = Cl.prettyPrint(value, 2); + expect(result).toStrictEqual(expected); + }); +});