Avatar

zackoverflow

18 public vals
Joined January 11, 2023

minizod

Tiny Zod implementation.

Why

Zod is a dense library, and its module structure (or lack thereof) makes it difficult for bundlers to tree-shake unused modules.

Additionally, using Zod in vals requires the await import syntax which means having to wrap every schema in a Promise and awaiting it. This is extremely annoying.

So this is a lil-tiny-smol Zod meant for use in vals. A noteworthy use-case is using minizod to generate tyep-safe API calls to run vals outside of Val Town (such as client-side).

Type-safe API call example

We can use minizod to create type safe HTTP handlers and generate the corresponding code to call them using Val Town's API, all in a type-safe manner.

First, create a schema for a function. The following example defines a schema for a function that takes a { name: string } parameter and returns a Promise<{ text: string }>.

const minizodExampleSchema = () =>
  @zackoverflow.minizod().chain((z) =>
    z
      .func()
      .args(z.tuple().item(z.object({ name: z.string() })))
      .ret(z.promise().return(z.object({ text: z.string() })))
  );

With a function schema, you can then create an implementation and export it as a val:

const minizodExample = @me.minizodExampleSchema().impl(async (
  { name },
) => ({ text: `Hello, ${name}!` })).json()

In the above example, we call .impl() on a function schema and pass in a closure which implements the actual body of the function. Here, we simply return a greeting to the name passed in.

We can call this val, and it will automatically parse and validate the args we give it:

// Errors at compile time and runtime for us!
const response = @me.minizodExample({ name: 420 })

Alternatively, we can use the .json() function to use it as a JSON HTTP handler:

const minizodExample = @me.minizodExampleSchema().impl(async (
  { name },
) => ({ text: `Hello, ${name}!` })).json() // <-- this part

We can now call minizodExample through Val Town's API. Since we defined a schema for it, we know exactly the types of its arguments and return, which means we can generate type-safe code to call the API:

let generatedApiCode =
  @zackoverflow.minizodFunctionGenerateTypescript(
    // put your username here
    "zackoverflow",
    "minizodExample",
    // put your auth token here
    "my auth token",
    @me.minizodExampleSchema(),
  );

This generates the following the code:

export const fetchMinizodExample = async (
  ...args: [{ name: string }]
): Promise<Awaited<Promise<{ text: string }>>> =>
  await fetch(`https://api.val.town/v1/run/zackoverflow.minizodExample`, {
    method: "POST",
    body: JSON.stringify({
      args: [...args],
    }),
    headers: {
      Authorization: "Bearer ksafajslfkjal;kjf;laksjl;fajsdf",
    },
  }).then((res) => res.json());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
export function minizod() {
type UnknownInput = unknown | undefined | null;
type ZType =
| ZAny
| ZUnknown
| ZPromise<ZType>
| ZFunction<ZTuple, ZType>
| ZString
| ZTuple<
[
ZType,
...ZType[],
] | []
>
| ZArray<ZType>
| ZObject<Record<string, ZType>>
| ZRecord<ZType>;
type Infer<T extends ZType> = T["type"] extends "unknown" ? unknown
: T["type"] extends "string" ? string
: T extends ZTuple ? InferTuple<T>
: T["type"] extends "object" ? InferObject<T>
: T extends ZRecord<ZType> ? InferRecord<T>
: T["type"] extends "array" ? InferArray<T>
: T["type"] extends "promise" ? InferPromise<T>
: T["type"] extends "any" ? any
: "invalid type";
type InferPromise<T> = T extends ZPromise<infer P extends ZType>
? Promise<Infer<P>>
: never;
type InferArray<T> = T extends ZArray<infer P extends ZType> ? Array<Infer<P>>
: never;
type InferRecord<T> = T extends ZRecord<infer V extends ZType>
? Record<string, Infer<V>>
: never;
type InferObject<T> = T extends
ZObject<infer Obj extends Record<string, ZType>> ? {
[k in keyof Obj]: Infer<Obj[k]>;
}
: never;
type InferFunction<T> = T extends ZFunction<
infer Args extends ZTuple<
[
ZType,
...ZType[],
] | []
>,
infer Ret extends ZType
> ? (...args: InferTuple<Args>) => Infer<Ret>
: never;
type InferForEach<T extends ZType[]> = T extends [] ? [] : T extends [
infer First extends ZType,
] ? [
Infer<First>,
]
: T extends [
infer First extends ZType,
...infer Rest extends ZType[],
] ? [
Infer<First>,
...InferForEach<Rest>,
]
: [];
type InferTuple<
T extends ZTuple<
[] | [
ZType,
...ZType[],
]
>,
> = T extends ZTuple<[]> ? [] : T extends ZTuple<[
infer First extends ZType,
...infer Rest extends ZType[],
]> ? [
Infer<First>,
...InferForEach<Rest>,
]
: [];
class ZRecord<V extends ZType> {
type: "record" = "record";
__vals: V;
constructor(__vals: V) {
this.__vals = __vals;
}
async parse(val: UnknownInput): Promise<Record<string, Infer<V>>> {
if (val === undefined || val === null)
throw new Error("undefined");
for (const key in val) {
if (typeof key !== "string")
throw new Error(`Key ${key} is not a string`);
// @ts-expect-error
await this.__vals.parse(val[key]);
}
return val as any;
}
parseSync(val: UnknownInput): Record<string, Infer<V>> {
if (val === undefined || val === null)
throw new Error("undefined");
for (const key in val) {
if (typeof key !== "string")
throw new Error(`Key ${key} is not a string`);

Subscribe to RSS feeds with e-mail notifications

This lets you subscribe to RSS feeds. It checks periodically for any new posts from any of your RSS feed subscriptions, and then sends you an e-mail with the link to the any new posts.

Getting started

1. Generate auth keys

Follow this to get your auth keys, and export your public keys. This will be used to e-mail yourself since @std.email is preferred over console.email

2. Create a @me.rssEmail val

You can do that by clicking this link and hitting 'Run'.

Or you can copy-paste this code into a new val:

const rssEmail = "you@youremail.com"

3. Fork this val

Hit 'Fork' on this val and run it. Then you can schedule the val to run every hour or whatever duration you'd like.

4. Add RSS feeds to @me.rssFeeds

If you look at your vals, you should find a new one called rssFeeds. It should look similar to this:

let rssFeeds = [
  "https://cprimozic.net/rss.xml",
  "https://matklad.github.io/feed.xml",
  "https://journal.stuffwithstuff.com/rss.xml",
  "https://lexi-lambda.github.io/feeds/all.rss.xml",
];

This is supposed to be an array containing the links of each RSS feed you'd like to subscribe to (in the form of JS strings).

To add RSS feeds, you can update this val by adding a new string containing the new RSS link.

Resetting the cache

If for any reason you would like to reset the cache, you can clear the keys of rssCache or use this convenience function to do so.

@zackoverflow.rssResetCache(@me.rssCache)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { email } from "https://esm.town/v/std/email?v=9";
import { rssEntriesToHTML } from "https://esm.town/v/zackoverflow/rssEntriesToHTML";
import { set } from "https://esm.town/v/std/set?v=11";
import { pollRss } from "https://esm.town/v/zackoverflow/pollRss";
import { rssEmail } from "https://esm.town/v/zackoverflow/rssEmail";
let { rssCache } = await import("https://esm.town/v/zackoverflow/rssCache");
let { rssFeeds } = await import("https://esm.town/v/zackoverflow/rssFeeds");
export const pollRssAndEmail = async () => {
rssFeeds = rssFeeds ??
["https://journal.stuffwithstuff.com/rss.xml"];
rssCache = rssCache ?? {} as any;
if (typeof rssEmail !== "string")
throw new Error(
"You haven't set your email, please create a val called @rssEmail and set its value to your e-mail address.",
);
const rssEntries = await pollRss(
rssFeeds,
rssCache,
);
await set("rssCache", rssCache);
if (rssEntries.length === 0)
return;
const html = await rssEntriesToHTML(rssEntries);
return await email({
to: [rssEmail],
subject: `RSS Update ${(new Date()).toDateString()}`,
html,
});
};

Lispaas (lisp as a service)

A mini lisp interpreter

How to use:

To execute code:

const result = @zackoverflow.lisp(" (+ 1 2)")

To just parse and return the AST:

const ast = @zackoverflow.lisp("(+ 1 2)", true)

The value returned is the last expression of the program, for example:

const lispResult = @zackoverflow.lisp("(+ 1 2) (+ 400 20)")
console.log('Val', lispResult.val === 420)

Example: Compute Fibonacci sequence

let result = @zackoverflow.lisp(`
(defun fib (x)
  (if (<= x 1)
    x
    (defun impl (i n-1 n-2)
        (if (= x i)
            (+ n-1 n-2)
            (impl (+ i 1) (+ n-1 n-2) n-1)))
    (impl 2 1 0)))

(assert-eq 0 (fib 0))
(assert-eq 1 (fib 1))
(assert-eq 1 (fib 2))
(assert-eq 2 (fib 3))
(assert-eq 3 (fib 4))
(assert-eq 5 (fib 5))
(assert-eq 8 (fib 6))
(assert-eq 13 (fib 7))
`);

Documentation

Functions

You can define a function like so:

(defun hello (x) (print x))

Rest/variadic arguments are also supported

(defun variable-amount-of-args (...args) (print args))

(variable-amount-of-args "Hello" "World!")

Lists

Define a list like so:

(let ((my-list (list 1 2 3 4)))
  (print my-list)
  (print (list-get my-list 1)))

Internally, a list is just a Javascript array. So indexing is O(1), but that does mean cdr requires copying (vs the linked list implementation).

Plists

Property lists, or records. Internally these are Javascript objects.

Create a plist like so:

(set null :key "Value")

TODO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
export const lisp = (src: string, onlyParse: boolean = false) => {
type LispValue = {
type: "null";
value: null;
} | {
type: "str";
value: string;
} | {
type: "num";
value: number;
} | {
type: "sym";
value: string;
} | {
type: "symp";
value: string;
} | {
type: "lambda";
value: Lambda;
} | {
type: "builtin";
value: string;
} | {
type: "plist";
value: Plist;
} | {
type: "list";
value: Array<LispValue>;
};
type List = Array<LispValue>;
type Plist = Record<string, LispValue>;
type Lambda = {
containsSpreadArg: boolean;
args: string[];
code: AstExpr;
};
type AstExpr =
| {
type: "str";
value: [
string,
];
}
| {
type: "num";
value: [
number,
];
}
| App
| {
type: "sym";
value: [
string,
];
}
| {
type: "symp";
value: [
string,
];
};
type App = {
type: "app";
value: Array<AstExpr>;
};
const makeStr = (value: string): AstExpr => ({
type: "str",
value: [value],
});
const makeNum = (value: number): AstExpr => ({
type: "num",
value: [value],
});
const makeSymp = (value: string): AstExpr => ({
type: "symp",
value: [value],
});
const makeSym = (value: string): AstExpr => {
return {
type: "sym",
value: [value],
};
};
const makeApply = (value: Array<AstExpr>): AstExpr => ({
type: "app",
value,
});
const parse = (src: string): Array<AstExpr> => {
let idx = 0;
const peek = (): string | undefined =>
idx >= src.length ? undefined : src[idx];
const matchEat = (expected: string): boolean | undefined => {
const char = peek();
if (char === undefined)
return undefined;
const match = char === expected;
if (match) {
idx += 1;
}
1
2
3
4
5
6
7
8
import { val as val2 } from "https://esm.town/v/neverstew/val?v=2";
import { vid } from "https://esm.town/v/stevekrouse/vid?v=4";
export const fetchValCode = async (valName: string) => {
const id = await vid(valName);
const val = await val2({ id });
return val.code;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { minizodCast } from "https://esm.town/v/zackoverflow/minizodCast";
export const minizodFunctionGenerateTypescript = (
username: string,
name: string,
authToken: string,
minizodType: any,
): string => {
const mzt = minizodCast(minizodType);
if (mzt.type !== "function")
throw new Error("Not a function");
const argsType = mzt.__args.toTs();
const retType = mzt.__ret.toTs();
const upperCamelCase = (str: string) => {
// Split the string by spaces or underscores
const words = str.split(/[\s/]/);
// Capitalize the first letter of each word and join them together
const camelCaseStr = words.map((word) =>
word.charAt(0).toUpperCase() + word.slice(1)
).join("");
return camelCaseStr;
};
return `
export const fetch${upperCamelCase(name)} =
async (...args: ${argsType}, method: "GET" | "POST" = "POST"): Promise<Awaited<${retType}>> =>
await fetch(\`https://api.val.town/v1/run/${username}.${name}\`, {
method,
body: JSON.stringify({
args: [...args],
}),
headers: {
Authorization: "Bearer ${authToken}",
},
}).then((res) => res.json());
`;
};
1
2
3
4
5
6
7
import { minizod } from "https://esm.town/v/zackoverflow/minizod";
export const minizodCast = (
minizodType: any,
): ReturnType<typeof minizod>["__ztype"] => {
return minizodType as any;
};
1
2
3
4
export const jsonResponse = (json: Record<any, any>) =>
new Response(JSON.stringify(json), {
headers: { "Content-Type": "application/json" },
});
1
2
3
4
5
import { minizodExampleSchema } from "https://esm.town/v/zackoverflow/minizodExampleSchema";
export const minizodExample = minizodExampleSchema().impl(async (
{ name },
) => ({ text: `Hello, ${name}!` })).json();
1
2
export const htmlResponse = (html: string) =>
new Response(html, { headers: { "Content-Type": "text/html" } });
1
2
3
4
5
6
7
8
9
import { minizod } from "https://esm.town/v/zackoverflow/minizod";
export const minizodExampleSchema = () =>
minizod().chain((z) =>
z
.func()
.args(z.tuple().item(z.object({ name: z.string() })))
.ret(z.promise().return(z.object({ text: z.string() })))
);