Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

143 lines · 4.0 KBRaw History
1/**
2 * GitHub Actions expression type coercion rules
3 * (https://docs.github.com/en/actions/learn-github-actions/expressions#operators).
4 */
5import { parseNumberLiteral } from "./lexer.js";
6
7export type Kind = "null" | "boolean" | "number" | "string" | "array" | "object";
8
9export function kindOf(v: unknown): Kind {
10 if (v === null || v === undefined) return "null";
11 switch (typeof v) {
12 case "boolean":
13 return "boolean";
14 case "number":
15 case "bigint":
16 return "number";
17 case "string":
18 return "string";
19 default:
20 return Array.isArray(v) ? "array" : "object";
21 }
22}
23
24export function isTruthy(v: unknown): boolean {
25 switch (kindOf(v)) {
26 case "null":
27 return false;
28 case "boolean":
29 return v as boolean;
30 case "number": {
31 const n = Number(v);
32 return n !== 0 && !Number.isNaN(n);
33 }
34 case "string":
35 return (v as string).length > 0;
36 default:
37 return true;
38 }
39}
40
41const NUMERIC_STRING = /^[+-]?(?:0[xX][0-9a-fA-F]+|0[oO][0-7]+|(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?|Infinity)$/;
42
43/** null -> 0, true -> 1, false -> 0, '' -> 0, numeric strings parsed, everything else NaN. */
44export function toNumber(v: unknown): number {
45 switch (kindOf(v)) {
46 case "null":
47 return 0;
48 case "boolean":
49 return v ? 1 : 0;
50 case "number":
51 return Number(v);
52 case "string": {
53 const s = (v as string).trim();
54 if (s === "") return 0;
55 if (!NUMERIC_STRING.test(s)) return NaN;
56 return parseNumberLiteral(s);
57 }
58 default:
59 return NaN;
60 }
61}
62
63/** Formats a number the way GitHub prints it (integers without decimals, up to 15 significant digits). */
64export function formatNumber(n: number): string {
65 if (Number.isNaN(n)) return "NaN";
66 if (n === Infinity) return "Infinity";
67 if (n === -Infinity) return "-Infinity";
68 if (Object.is(n, -0)) return "0";
69 if (Number.isInteger(n) && Math.abs(n) < 1e21) return n.toFixed(0);
70 return String(parseFloat(n.toPrecision(15)));
71}
72
73/** String coercion used by functions (contains, format, join...). Arrays/objects become 'Array'/'Object'. */
74export function toStr(v: unknown): string {
75 switch (kindOf(v)) {
76 case "null":
77 return "";
78 case "boolean":
79 return v ? "true" : "false";
80 case "number":
81 return formatNumber(Number(v));
82 case "string":
83 return v as string;
84 case "array":
85 return "Array";
86 default:
87 return "Object";
88 }
89}
90
91/** String coercion used when interpolating `${{ }}` into a template: objects/arrays become JSON. */
92export function toTemplateString(v: unknown): string {
93 const k = kindOf(v);
94 if (k === "array" || k === "object") return toJSONString(v);
95 return toStr(v);
96}
97
98export function toJSONString(v: unknown): string {
99 return JSON.stringify(v === undefined ? null : v, (_k, val: unknown) => (typeof val === "bigint" ? Number(val) : val), 2) ?? "null";
100}
101
102const fold = (s: string) => s.toUpperCase();
103
104/** GitHub loose equality (`==`). */
105export function looseEquals(a: unknown, b: unknown): boolean {
106 const ka = kindOf(a);
107 const kb = kindOf(b);
108 if (ka !== kb) {
109 const na = toNumber(a);
110 const nb = toNumber(b);
111 return na === nb;
112 }
113 switch (ka) {
114 case "null":
115 return true;
116 case "number":
117 return Number(a) === Number(b);
118 case "string":
119 return fold(a as string) === fold(b as string);
120 case "boolean":
121 return a === b;
122 default:
123 return a === b;
124 }
125}
126
127/** Returns <0, 0, >0, or NaN when the values are not comparable. */
128export function looseCompare(a: unknown, b: unknown): number {
129 const ka = kindOf(a);
130 const kb = kindOf(b);
131 if (ka !== kb || ka === "null" || ka === "boolean" || ka === "number") {
132 const na = toNumber(a);
133 const nb = toNumber(b);
134 if (Number.isNaN(na) || Number.isNaN(nb)) return NaN;
135 return na === nb ? 0 : na < nb ? -1 : 1;
136 }
137 if (ka === "string") {
138 const sa = fold(a as string);
139 const sb = fold(b as string);
140 return sa === sb ? 0 : sa < sb ? -1 : 1;
141 }
142 return NaN;
143}