Avatar

@zackoverflow

10 likes19 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());
Readme
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
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 [

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)
Readme
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

Readme
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
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
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;
};

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.

This an alternate version of @zackoverflow.pollRssAndEmail which does not require using Val Town auth. Meaning it uses console.email instead of @std.email, but does not allow you to specify which e-mail to send to.

Getting started

1. 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.

2. 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)
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { rssEntriesToHTML } from "https://esm.town/v/zackoverflow/rssEntriesToHTML";
import { pollRss } from "https://esm.town/v/zackoverflow/pollRss";
let { rssCache } = await import("https://esm.town/v/zackoverflow/rssCache");
let { rssFeeds } = await import("https://esm.town/v/zackoverflow/rssFeeds");
export const pollRssAndEmailNoAuth = async () => {
rssFeeds = rssFeeds ??
["https://journal.stuffwithstuff.com/rss.xml"];
rssCache = rssCache ?? {} as any;
const rssEntries = await pollRss(
rssFeeds,
rssCache,
);
if (rssEntries.length === 0)
return;
const html = await rssEntriesToHTML(rssEntries);
return console.email({ html }, `RSS Update ${(new Date()).toDateString()}`);
};
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" } });