Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Cl.prettyPrint #1551

Merged
merged 3 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from all 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 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';

hugocaillard marked this conversation as resolved.
Show resolved Hide resolved
// 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);
janniks marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* @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);
});
});
Loading