Skip to content

Commit 83227f3

Browse files
committed
refactor: update AssetName to Effect
1 parent 4d59aec commit 83227f3

3 files changed

Lines changed: 277 additions & 0 deletions

File tree

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { Data, FastCheck, Inspectable, pipe, Schema } from "effect";
2+
import * as Hex from "./Hex.js";
3+
4+
/**
5+
* CDDL specs
6+
* asset_name = bytes .size (0 .. 32)
7+
*/
8+
9+
/**
10+
* The maximum length in bytes of an asset name.
11+
*
12+
* @since 2.0.0
13+
* @category constants
14+
*/
15+
export const ASSET_NAME_MAX_LENGTH = 32;
16+
17+
/**
18+
* Error class for AssetName related operations.
19+
*
20+
* @example
21+
* import { AssetName } from "@lucid-evolution/experimental";
22+
* import assert from "assert";
23+
*
24+
* const error = new AssetName.AssetNameError({ message: "Invalid asset name" });
25+
* assert(error.message === "Invalid asset name");
26+
*
27+
* @since 2.0.0
28+
* @category errors
29+
*/
30+
export class AssetNameError extends Data.TaggedError("AssetNameError")<{
31+
message?: string;
32+
reason?: "InvalidBytesLength" | "InvalidHexFormat" | "InvalidCBORFormat";
33+
}> {}
34+
35+
/**
36+
* Schema for validating AssetName bytes with CDDL constraint (0..32 bytes)
37+
*
38+
* @since 2.0.0
39+
* @category schemas
40+
*/
41+
export const AssetNameBytes = pipe(
42+
Schema.Uint8Array,
43+
Schema.filter((a) => a.length >= 0 && a.length <= ASSET_NAME_MAX_LENGTH),
44+
Schema.typeSchema,
45+
).annotations({
46+
message: (issue) =>
47+
`Asset name must be a byte array of length 0-${ASSET_NAME_MAX_LENGTH}, got length ${
48+
(issue.actual as Uint8Array)?.length
49+
}`,
50+
identifier: "AssetNameBytes",
51+
});
52+
53+
export declare const NominalType: unique symbol;
54+
export interface AssetName {
55+
readonly [NominalType]: unique symbol;
56+
}
57+
58+
/**
59+
* Schema for AssetName representing an asset name.
60+
* Follows CDDL specification: bytes .size (0 .. 32)
61+
*
62+
* @since 2.0.0
63+
* @category schemas
64+
*/
65+
export class AssetName extends Schema.TaggedClass<AssetName>("AssetName")(
66+
"AssetName",
67+
{
68+
bytes: AssetNameBytes,
69+
},
70+
) {
71+
[Inspectable.NodeInspectSymbol]() {
72+
return {
73+
_tag: "AssetName",
74+
bytes: this.bytes,
75+
};
76+
}
77+
}
78+
79+
/**
80+
* Check if the given value is a valid AssetName
81+
*
82+
* @since 2.0.0
83+
* @category predicates
84+
*/
85+
export const isAssetName = Schema.is(AssetName);
86+
87+
/**
88+
* Schema for transforming between bytes and AssetName
89+
*
90+
* @since 2.0.0
91+
* @category encoding/decoding
92+
*/
93+
export const Bytes = Schema.transform(AssetNameBytes, AssetName, {
94+
strict: true,
95+
encode: (_, toA) => toA.bytes,
96+
decode: (fromA) => new AssetName({ bytes: fromA }),
97+
});
98+
99+
/**
100+
* Schema for transforming between hex string and AssetName
101+
*
102+
* @since 2.0.0
103+
* @category encoding/decoding
104+
*/
105+
export const HexString = Schema.transform(Hex.HexString, AssetName, {
106+
strict: true,
107+
encode: (_toI, toA) => Hex.fromBytes(toA.bytes),
108+
decode: (fromI, _fromA) => new AssetName({ bytes: Hex.toBytes(fromI) }),
109+
});
110+
111+
/**
112+
* Check if two AssetName instances are equal.
113+
*
114+
* @example
115+
* import { AssetName } from "@lucid-evolution/experimental";
116+
* import assert from "assert";
117+
*
118+
* const name1 = new AssetName({ bytes: new Uint8Array([0x74, 0x6f, 0x6b, 0x65, 0x6e]) });
119+
* const name2 = new AssetName({ bytes: new Uint8Array([0x74, 0x6f, 0x6b, 0x65, 0x6e]) });
120+
* const name3 = new AssetName({ bytes: new Uint8Array([0x6f, 0x74, 0x68, 0x65, 0x72]) });
121+
*
122+
* assert(AssetName.equals(name1, name2) === true);
123+
* assert(AssetName.equals(name1, name3) === false);
124+
*
125+
* @since 2.0.0
126+
* @category equality
127+
*/
128+
export const equals = (a: AssetName, b: AssetName): boolean =>
129+
a.bytes.length === b.bytes.length &&
130+
a.bytes.every((byte, index) => byte === b.bytes[index]);
131+
132+
/**
133+
* Generator for creating random AssetName instances for testing
134+
*
135+
* @example
136+
* import { AssetName } from "@lucid-evolution/experimental";
137+
* import { FastCheck } from "effect";
138+
* import assert from "assert";
139+
*
140+
* const randomSamples = FastCheck.sample(AssetName.generator, 10);
141+
* randomSamples.forEach((assetName) => {
142+
* assert(assetName instanceof AssetName);
143+
* assert(assetName.bytes.length <= 32);
144+
* });
145+
*
146+
* @since 2.0.0
147+
* @category generators
148+
*/
149+
export const generator = FastCheck.uint8Array({
150+
minLength: 0,
151+
maxLength: ASSET_NAME_MAX_LENGTH,
152+
}).map((bytes) => new AssetName({ bytes }));
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from "vitest";
2+
import { FastCheck, Schema } from "effect";
3+
import * as AssetName from "../src/AssetName.js";
4+
5+
describe("AssetName property tests", () => {
6+
describe("AssetName", () => {
7+
it("should be a valid HexString asset name", () => {
8+
FastCheck.assert(
9+
FastCheck.property(
10+
AssetName.generator.filter((assetName) => assetName.bytes.length > 0), // Filter out empty for hex
11+
(assetName) => {
12+
const hexString = Schema.encodeSync(AssetName.HexString)(assetName);
13+
const decoded = Schema.decodeSync(AssetName.HexString)(hexString);
14+
expect(decoded).toEqual(assetName);
15+
},
16+
),
17+
);
18+
});
19+
20+
it("should be a valid Bytes asset name", () => {
21+
FastCheck.assert(
22+
FastCheck.property(AssetName.generator, (assetName) => {
23+
const bytes = Schema.encodeSync(AssetName.Bytes)(assetName);
24+
const decoded = Schema.decodeSync(AssetName.Bytes)(bytes);
25+
expect(decoded).toEqual(assetName);
26+
}),
27+
);
28+
});
29+
});
30+
});
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { describe, expect, it } from "vitest";
2+
import { FastCheck, Schema } from "effect";
3+
import * as AssetName from "../src/AssetName.js";
4+
5+
// Sample asset names for testing - organized by validity
6+
const VALID_ASSET_NAME_BYTES = [
7+
new Uint8Array([]),
8+
new Uint8Array([0x74, 0x6f, 0x6b, 0x65, 0x6e]),
9+
new Uint8Array([0x61, 0x73, 0x73, 0x65, 0x74]),
10+
new Uint8Array(32).fill(0x41),
11+
];
12+
13+
const VALID_ASSET_NAME_HEX = ["746f6b656e", "6173736574", "41".repeat(32)];
14+
15+
const INVALID_ASSET_NAME_BYTES = [new Uint8Array(33), new Uint8Array(50)];
16+
17+
const INVALID_ASSET_NAME_HEX = [
18+
"41".repeat(34),
19+
"g746f6b656e",
20+
"746f6b656",
21+
"",
22+
];
23+
24+
/**
25+
* Tests for the AssetName functionality -
26+
* focusing on asset name creation, validation, and serialization
27+
*/
28+
describe("AssetName Validation", () => {
29+
it.each(VALID_ASSET_NAME_BYTES.map((bytes, index) => [bytes, index]))(
30+
"should create valid AssetName from bytes (case %s)",
31+
(bytes) => {
32+
const assetName = new AssetName.AssetName({ bytes });
33+
expect(assetName._tag).toBe("AssetName");
34+
expect(assetName.bytes).toEqual(bytes);
35+
expect(AssetName.isAssetName(assetName)).toBe(true);
36+
},
37+
);
38+
39+
it.each(VALID_ASSET_NAME_HEX.map((hex, index) => [hex, index]))(
40+
"should create valid AssetName from hex string (case %s)",
41+
(hex) => {
42+
const assetName = Schema.decodeUnknownSync(AssetName.HexString)(hex);
43+
expect(assetName._tag).toBe("AssetName");
44+
expect(AssetName.isAssetName(assetName)).toBe(true);
45+
},
46+
);
47+
48+
it.each(INVALID_ASSET_NAME_BYTES.map((bytes, index) => [bytes, index]))(
49+
"should throw on invalid asset name bytes (case %s)",
50+
(bytes) => {
51+
expect(() => new AssetName.AssetName({ bytes })).toThrow();
52+
},
53+
);
54+
55+
it.each(INVALID_ASSET_NAME_HEX.map((hex, index) => [hex, index]))(
56+
"should throw on invalid hex string (case %s)",
57+
(hex) => {
58+
expect(() =>
59+
Schema.decodeUnknownSync(AssetName.HexString)(hex),
60+
).toThrow();
61+
},
62+
);
63+
64+
it("should validate length constants", () => {
65+
expect(AssetName.ASSET_NAME_MAX_LENGTH).toBe(32);
66+
});
67+
68+
it("should check equality correctly", () => {
69+
const name1 = new AssetName.AssetName({
70+
bytes: new Uint8Array([0x74, 0x6f, 0x6b, 0x65, 0x6e]),
71+
});
72+
const name2 = new AssetName.AssetName({
73+
bytes: new Uint8Array([0x74, 0x6f, 0x6b, 0x65, 0x6e]),
74+
});
75+
const name3 = new AssetName.AssetName({
76+
bytes: new Uint8Array([0x61, 0x73, 0x73, 0x65, 0x74]),
77+
});
78+
79+
expect(AssetName.equals(name1, name2)).toBe(true);
80+
expect(AssetName.equals(name1, name3)).toBe(false);
81+
});
82+
83+
it("should generate valid AssetName instances", () => {
84+
const samples = FastCheck.sample(AssetName.generator, 20);
85+
86+
samples.forEach((assetName) => {
87+
expect(assetName).toBeInstanceOf(AssetName.AssetName);
88+
expect(assetName._tag).toBe("AssetName");
89+
expect(assetName.bytes.length).toBeLessThanOrEqual(
90+
AssetName.ASSET_NAME_MAX_LENGTH,
91+
);
92+
expect(AssetName.isAssetName(assetName)).toBe(true);
93+
});
94+
});
95+
});

0 commit comments

Comments
 (0)