Skip to content

Commit b3c84c9

Browse files
committed
feat: try out io-ts and zod tranformers
1 parent d823125 commit b3c84c9

File tree

6 files changed

+159
-1
lines changed

6 files changed

+159
-1
lines changed

bun.lockb

1.01 KB
Binary file not shown.

package.json

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
"import": "./dist/esm/secret.js",
1111
"require": "./dist/cjs/secret.js",
1212
"types": "./dist/dts/secret.d.ts"
13+
},
14+
"./io-ts": {
15+
"import": "./dist/esm/io-ts/secret.js",
16+
"require": "./dist/cjs/io-ts/secret.js",
17+
"types": "./dist/dts/io-ts/secret.d.ts"
18+
},
19+
"./zod": {
20+
"import": "./dist/esm/zod/secret.js",
21+
"require": "./dist/cjs/zod/secret.js",
22+
"types": "./dist/dts/zod/secret.d.ts"
1323
}
1424
},
1525
"files": [
@@ -25,7 +35,10 @@
2535
"@types/bun": "latest",
2636
"babel-plugin-annotate-pure-calls": "^0.4.0",
2737
"fast-check": "^3.17.1",
28-
"typescript": "^5.4.5"
38+
"fp-ts": "^2.16.5",
39+
"io-ts": "^2.2.21",
40+
"typescript": "^5.4.5",
41+
"zod": "^3.22.4"
2942
},
3043
"description": "`secret` is a simple utility libraty for managing secrets in a TypeScript app.",
3144
"repository": {

src/io-ts/io-st-secret.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as fc from "fast-check";
2+
import { expect, describe, test } from "bun:test";
3+
import * as SecretValue from "../secret";
4+
import { SecretCodec, StringToSecret, SecretToExposedString } from "./secret";
5+
import { pipe } from "fp-ts/function";
6+
import { fold } from "fp-ts/Either";
7+
8+
describe("secret encode/decode io-ts codec", () => {
9+
test("should decode a string to secret", () => {
10+
fc.assert(
11+
fc.property(fc.string({ minLength: 1 }), (input) => {
12+
pipe(
13+
input,
14+
StringToSecret.decode,
15+
fold(
16+
() => {
17+
expect.unreachable("decoding should not fail");
18+
},
19+
(s) => {
20+
expect(SecretValue.isSecret(s)).toBe(true);
21+
expect(SecretValue.expose(s)).toEqual(input);
22+
}
23+
)
24+
);
25+
})
26+
);
27+
});
28+
29+
test("should encode secret to exposed string", () => {
30+
fc.assert(
31+
fc.property(fc.string({ minLength: 1 }), (input) => {
32+
const secret = SecretValue.fromString(input);
33+
const encoded = SecretToExposedString.encode(secret);
34+
35+
expect(encoded).toEqual(input);
36+
})
37+
);
38+
});
39+
40+
test("should decode a string to secret and back to original", () => {
41+
fc.assert(
42+
fc.property(fc.string({ minLength: 1 }), (input) => {
43+
pipe(
44+
input,
45+
SecretCodec.decode,
46+
fold(
47+
() => {
48+
expect.unreachable("decoding should not fail");
49+
},
50+
(s) => {
51+
const encoded = SecretCodec.encode(s);
52+
expect(encoded).toEqual(input);
53+
}
54+
)
55+
);
56+
})
57+
);
58+
});
59+
});

src/io-ts/secret.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as SecretInternal from "../secret";
2+
import { make, type Codec } from "io-ts/Codec";
3+
import {
4+
string as Dstring,
5+
parse,
6+
success,
7+
failure,
8+
type Decoder,
9+
type TypeOf,
10+
} from "io-ts/Decoder";
11+
import { type Encoder as IOTSEncoder, type OutputOf } from "io-ts/Encoder";
12+
import { pipe } from "fp-ts/function";
13+
14+
export const StringToSecret: Decoder<unknown, SecretInternal.Secret> = pipe(
15+
Dstring,
16+
parse((s) => {
17+
const secret = SecretInternal.fromString(s);
18+
return SecretInternal.isSecret(secret)
19+
? success(secret)
20+
: failure(
21+
s,
22+
`cannot decode given value, should be parsable into a secret`
23+
);
24+
})
25+
);
26+
27+
export type StringToSecret = TypeOf<typeof StringToSecret>;
28+
29+
export const SecretToExposedString: IOTSEncoder<string, SecretInternal.Secret> =
30+
{
31+
encode: SecretInternal.expose,
32+
};
33+
34+
export type SecretToExposedString = OutputOf<typeof SecretToExposedString>;
35+
36+
export const SecretCodec: Codec<unknown, string, SecretInternal.Secret> = make(
37+
StringToSecret,
38+
SecretToExposedString
39+
);

src/zod/secret.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import * as SecretInternal from "../secret";
2+
import { z } from "zod";
3+
4+
export const Secret = z
5+
.string()
6+
.transform((str) => SecretInternal.fromString(str));
7+
8+
export type Secret = z.infer<typeof Secret>;

src/zod/zod-secret.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as fc from "fast-check";
2+
import { expect, describe, test } from "bun:test";
3+
import * as SecretValue from "../secret";
4+
import { Secret as SecretSchema } from "./secret";
5+
import { z } from "zod";
6+
7+
describe("secret encode/decode zod", () => {
8+
test("should encode a string to secret and back to original", () => {
9+
fc.assert(
10+
fc.property(fc.string({ minLength: 1 }), (input) => {
11+
const secret = SecretSchema.safeParse(input);
12+
13+
if (secret.success) {
14+
expect(SecretValue.isSecret(secret.data)).toBe(true);
15+
const exposed = SecretValue.expose(secret.data);
16+
expect(exposed).toEqual(input);
17+
}
18+
})
19+
);
20+
});
21+
22+
test("should encode struct values to secrets", () => {
23+
fc.assert(
24+
fc.property(fc.string({ minLength: 1 }), (input) => {
25+
const struct = z.object({
26+
redacted: SecretSchema,
27+
});
28+
29+
const result = struct.safeParse({ redacted: input });
30+
31+
if (result.success) {
32+
expect(SecretValue.isSecret(result.data.redacted)).toBe(true);
33+
const exposed = SecretValue.expose(result.data.redacted);
34+
expect(exposed).toEqual(input);
35+
}
36+
})
37+
);
38+
});
39+
});

0 commit comments

Comments
 (0)