Skip to content

Commit

Permalink
feat: add Cl.prettyPrint (#1551)
Browse files Browse the repository at this point in the history
* feat: add pretty print

* docs: improve Cl.prettyPrint docs

* refactor: review
  • Loading branch information
hugocaillard committed Sep 20, 2023
1 parent 04b96d1 commit ae25ad9
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/transactions/src/cl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
uintCV,
} from './clarity';

export { prettyPrint } from './clarity/prettyPrint';

// todo: https://github.com/hirosystems/clarinet/issues/786

// Primitives //////////////////////////////////////////////////////////////////
Expand Down
128 changes: 128 additions & 0 deletions packages/transactions/src/clarity/prettyPrint.ts
Original file line number Diff line number Diff line change
@@ -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);
}
136 changes: 136 additions & 0 deletions packages/transactions/tests/prettyPrint.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit ae25ad9

Please sign in to comment.