@nudojs/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -0
- package/dist/index.d.ts +34 -1
- package/dist/index.js +312 -1
- package/package.json +11 -1
- package/src/__tests__/ops-refined.test.ts +194 -0
- package/src/__tests__/type-value-refined.test.ts +210 -0
- package/src/index.ts +19 -1
- package/src/ops.ts +65 -2
- package/src/refinements/index.ts +2 -0
- package/src/refinements/range.ts +86 -0
- package/src/refinements/template.ts +178 -0
- package/src/type-value.ts +40 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { type TypeValue, type Refinement, T, typeValueToString } from "../type-value.ts";
|
|
2
|
+
import { createRange } from "./range.ts";
|
|
3
|
+
|
|
4
|
+
export function getKnownPrefix(parts: TypeValue[]): string {
|
|
5
|
+
let prefix = "";
|
|
6
|
+
for (const p of parts) {
|
|
7
|
+
if (p.kind === "literal" && typeof p.value === "string") {
|
|
8
|
+
prefix += p.value;
|
|
9
|
+
} else {
|
|
10
|
+
break;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return prefix;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getKnownSuffix(parts: TypeValue[]): string {
|
|
17
|
+
let suffix = "";
|
|
18
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
19
|
+
const p = parts[i];
|
|
20
|
+
if (p.kind === "literal" && typeof p.value === "string") {
|
|
21
|
+
suffix = p.value + suffix;
|
|
22
|
+
} else {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return suffix;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFixedLength(parts: TypeValue[]): number {
|
|
30
|
+
let len = 0;
|
|
31
|
+
for (const p of parts) {
|
|
32
|
+
if (p.kind === "literal" && typeof p.value === "string") {
|
|
33
|
+
len += p.value.length;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return len;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getAllFixedText(parts: TypeValue[]): string {
|
|
40
|
+
return parts
|
|
41
|
+
.filter((p): p is TypeValue & { kind: "literal" } => p.kind === "literal" && typeof p.value === "string")
|
|
42
|
+
.map((p) => p.value as string)
|
|
43
|
+
.join("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatTemplateName(parts: TypeValue[]): string {
|
|
47
|
+
const inner = parts
|
|
48
|
+
.map((p) => (p.kind === "literal" && typeof p.value === "string" ? p.value : `\${${typeValueToString(p)}}`))
|
|
49
|
+
.join("");
|
|
50
|
+
return `\`${inner}\``;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeParts(parts: TypeValue[]): TypeValue[] {
|
|
54
|
+
const result: TypeValue[] = [];
|
|
55
|
+
for (const p of parts) {
|
|
56
|
+
const last = result[result.length - 1];
|
|
57
|
+
if (
|
|
58
|
+
last?.kind === "literal" && typeof last.value === "string" &&
|
|
59
|
+
p.kind === "literal" && typeof p.value === "string"
|
|
60
|
+
) {
|
|
61
|
+
result[result.length - 1] = T.literal(last.value + p.value);
|
|
62
|
+
} else if (
|
|
63
|
+
last?.kind === "primitive" && last.type === "string" &&
|
|
64
|
+
p.kind === "primitive" && p.type === "string"
|
|
65
|
+
) {
|
|
66
|
+
// T.string + T.string collapses to T.string
|
|
67
|
+
} else {
|
|
68
|
+
result.push(p);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createTemplateRefinement(parts: TypeValue[]): Refinement {
|
|
75
|
+
return {
|
|
76
|
+
name: formatTemplateName(parts),
|
|
77
|
+
meta: { parts },
|
|
78
|
+
check(value: unknown) {
|
|
79
|
+
if (typeof value !== "string") return false;
|
|
80
|
+
return matchesTemplate(value, parts);
|
|
81
|
+
},
|
|
82
|
+
ops: {
|
|
83
|
+
"+"(self: TypeValue, other: TypeValue) {
|
|
84
|
+
return concatTemplates(self, other);
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
methods: {
|
|
88
|
+
startsWith(_self: TypeValue, args: TypeValue[]) {
|
|
89
|
+
const arg = args[0];
|
|
90
|
+
if (arg?.kind !== "literal" || typeof arg.value !== "string") return undefined;
|
|
91
|
+
const prefix = getKnownPrefix((_self as any).refinement.meta.parts as TypeValue[]);
|
|
92
|
+
const search = arg.value;
|
|
93
|
+
if (prefix.length >= search.length) {
|
|
94
|
+
return T.literal(prefix.startsWith(search));
|
|
95
|
+
}
|
|
96
|
+
if (search.startsWith(prefix)) return undefined;
|
|
97
|
+
return T.literal(false);
|
|
98
|
+
},
|
|
99
|
+
endsWith(_self: TypeValue, args: TypeValue[]) {
|
|
100
|
+
const arg = args[0];
|
|
101
|
+
if (arg?.kind !== "literal" || typeof arg.value !== "string") return undefined;
|
|
102
|
+
const suffix = getKnownSuffix((_self as any).refinement.meta.parts as TypeValue[]);
|
|
103
|
+
const search = arg.value;
|
|
104
|
+
if (suffix.length >= search.length) {
|
|
105
|
+
return T.literal(suffix.endsWith(search));
|
|
106
|
+
}
|
|
107
|
+
if (search.endsWith(suffix)) return undefined;
|
|
108
|
+
return T.literal(false);
|
|
109
|
+
},
|
|
110
|
+
includes(_self: TypeValue, args: TypeValue[]) {
|
|
111
|
+
const arg = args[0];
|
|
112
|
+
if (arg?.kind !== "literal" || typeof arg.value !== "string") return undefined;
|
|
113
|
+
const fixed = getAllFixedText((_self as any).refinement.meta.parts as TypeValue[]);
|
|
114
|
+
if (fixed.includes(arg.value)) return T.literal(true);
|
|
115
|
+
return undefined;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
properties: {
|
|
119
|
+
length(_self: TypeValue) {
|
|
120
|
+
const parts = (_self as any).refinement.meta.parts as TypeValue[];
|
|
121
|
+
const hasAbstract = parts.some((p) => p.kind !== "literal");
|
|
122
|
+
if (!hasAbstract) return undefined;
|
|
123
|
+
const minLen = getFixedLength(parts);
|
|
124
|
+
return createRange({ min: minLen });
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function matchesTemplate(value: string, parts: TypeValue[]): boolean {
|
|
131
|
+
let pos = 0;
|
|
132
|
+
for (let i = 0; i < parts.length; i++) {
|
|
133
|
+
const p = parts[i];
|
|
134
|
+
if (p.kind === "literal" && typeof p.value === "string") {
|
|
135
|
+
if (!value.startsWith(p.value, pos)) return false;
|
|
136
|
+
pos += p.value.length;
|
|
137
|
+
} else {
|
|
138
|
+
if (i === parts.length - 1) return true;
|
|
139
|
+
const next = parts[i + 1];
|
|
140
|
+
if (next?.kind === "literal" && typeof next.value === "string") {
|
|
141
|
+
const idx = value.indexOf(next.value, pos);
|
|
142
|
+
if (idx === -1) return false;
|
|
143
|
+
pos = idx;
|
|
144
|
+
} else {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return pos === value.length;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function createTemplate(parts: TypeValue[]): TypeValue {
|
|
153
|
+
const normalized = normalizeParts(parts);
|
|
154
|
+
if (normalized.length === 1 && normalized[0].kind === "literal") {
|
|
155
|
+
return normalized[0];
|
|
156
|
+
}
|
|
157
|
+
if (normalized.length === 1 && normalized[0].kind === "primitive" && normalized[0].type === "string") {
|
|
158
|
+
return T.string;
|
|
159
|
+
}
|
|
160
|
+
return T.refine(T.string, createTemplateRefinement(normalized));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isTemplate(tv: TypeValue): boolean {
|
|
164
|
+
return tv.kind === "refined" && Array.isArray(tv.refinement.meta.parts);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getTemplateParts(tv: TypeValue): TypeValue[] | undefined {
|
|
168
|
+
if (tv.kind === "refined" && Array.isArray(tv.refinement.meta.parts)) {
|
|
169
|
+
return tv.refinement.meta.parts as TypeValue[];
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function concatTemplates(left: TypeValue, right: TypeValue): TypeValue {
|
|
175
|
+
const leftParts = getTemplateParts(left) ?? [left];
|
|
176
|
+
const rightParts = getTemplateParts(right) ?? [right];
|
|
177
|
+
return createTemplate([...leftParts, ...rightParts]);
|
|
178
|
+
}
|
package/src/type-value.ts
CHANGED
|
@@ -5,12 +5,22 @@ import type { Environment } from "./environment.ts";
|
|
|
5
5
|
|
|
6
6
|
export type LiteralValue = string | number | boolean | null | undefined;
|
|
7
7
|
|
|
8
|
+
export type Refinement = {
|
|
9
|
+
name: string;
|
|
10
|
+
meta: Record<string, unknown>;
|
|
11
|
+
check?: (value: unknown) => boolean;
|
|
12
|
+
ops?: Record<string, (self: TypeValue, other: TypeValue) => TypeValue | undefined>;
|
|
13
|
+
methods?: Record<string, (self: TypeValue, args: TypeValue[]) => TypeValue | undefined>;
|
|
14
|
+
properties?: Record<string, (self: TypeValue) => TypeValue | undefined>;
|
|
15
|
+
};
|
|
16
|
+
|
|
8
17
|
export type TypeValue =
|
|
9
18
|
| { kind: "literal"; value: LiteralValue }
|
|
10
19
|
| {
|
|
11
20
|
kind: "primitive";
|
|
12
21
|
type: "number" | "string" | "boolean" | "bigint" | "symbol";
|
|
13
22
|
}
|
|
23
|
+
| { kind: "refined"; base: TypeValue; refinement: Refinement }
|
|
14
24
|
| { kind: "object"; properties: Record<string, TypeValue>; id: symbol }
|
|
15
25
|
| { kind: "array"; element: TypeValue }
|
|
16
26
|
| { kind: "tuple"; elements: TypeValue[] }
|
|
@@ -68,6 +78,11 @@ export const T = {
|
|
|
68
78
|
body,
|
|
69
79
|
closure,
|
|
70
80
|
}),
|
|
81
|
+
refine: (base: TypeValue, refinement: Refinement): TypeValue => ({
|
|
82
|
+
kind: "refined",
|
|
83
|
+
base,
|
|
84
|
+
refinement,
|
|
85
|
+
}),
|
|
71
86
|
} as const;
|
|
72
87
|
|
|
73
88
|
// --- Helpers ---
|
|
@@ -78,6 +93,9 @@ export function typeValueEquals(a: TypeValue, b: TypeValue): boolean {
|
|
|
78
93
|
if (a.kind === "primitive" && b.kind === "primitive") return a.type === b.type;
|
|
79
94
|
if (a.kind === "never" && b.kind === "never") return true;
|
|
80
95
|
if (a.kind === "unknown" && b.kind === "unknown") return true;
|
|
96
|
+
if (a.kind === "refined" && b.kind === "refined") {
|
|
97
|
+
return a.refinement.name === b.refinement.name && typeValueEquals(a.base, b.base);
|
|
98
|
+
}
|
|
81
99
|
if (a.kind === "promise" && b.kind === "promise") {
|
|
82
100
|
return typeValueEquals(a.value, b.value);
|
|
83
101
|
}
|
|
@@ -141,6 +159,18 @@ export function isSubtypeOf(a: TypeValue, b: TypeValue): boolean {
|
|
|
141
159
|
return false;
|
|
142
160
|
}
|
|
143
161
|
|
|
162
|
+
if (a.kind === "literal" && b.kind === "refined") {
|
|
163
|
+
if (!isSubtypeOf(a, b.base)) return false;
|
|
164
|
+
return b.refinement.check ? b.refinement.check(a.value) : false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (a.kind === "refined") {
|
|
168
|
+
if (b.kind === "refined" && a.refinement.name === b.refinement.name) {
|
|
169
|
+
return isSubtypeOf(a.base, b.base);
|
|
170
|
+
}
|
|
171
|
+
return isSubtypeOf(a.base, b);
|
|
172
|
+
}
|
|
173
|
+
|
|
144
174
|
if (a.kind === "object" && b.kind === "object") {
|
|
145
175
|
return Object.entries(b.properties).every(
|
|
146
176
|
([k, bv]) => k in a.properties && isSubtypeOf(a.properties[k], bv),
|
|
@@ -226,6 +256,9 @@ export function deepCloneTypeValue(tv: TypeValue, idMap?: Map<symbol, symbol>):
|
|
|
226
256
|
}
|
|
227
257
|
return { kind: "instance", className: tv.className, properties: newProps };
|
|
228
258
|
}
|
|
259
|
+
if (tv.kind === "refined") {
|
|
260
|
+
return { kind: "refined", base: deepCloneTypeValue(tv.base, map), refinement: tv.refinement };
|
|
261
|
+
}
|
|
229
262
|
if (tv.kind === "union") {
|
|
230
263
|
return simplifyUnion(tv.members.map((m) => deepCloneTypeValue(m, map)));
|
|
231
264
|
}
|
|
@@ -261,6 +294,8 @@ export function typeValueToString(tv: TypeValue): string {
|
|
|
261
294
|
}
|
|
262
295
|
case "primitive":
|
|
263
296
|
return tv.type;
|
|
297
|
+
case "refined":
|
|
298
|
+
return tv.refinement.name;
|
|
264
299
|
case "object": {
|
|
265
300
|
const entries = Object.entries(tv.properties);
|
|
266
301
|
if (entries.length === 0) return "{}";
|
|
@@ -323,6 +358,7 @@ export function getPrimitiveTypeOf(tv: TypeValue): string | undefined {
|
|
|
323
358
|
return typeof v;
|
|
324
359
|
}
|
|
325
360
|
if (tv.kind === "primitive") return tv.type;
|
|
361
|
+
if (tv.kind === "refined") return getPrimitiveTypeOf(tv.base);
|
|
326
362
|
if (tv.kind === "object") return "object";
|
|
327
363
|
if (tv.kind === "array" || tv.kind === "tuple") return "object";
|
|
328
364
|
if (tv.kind === "function") return "function";
|
|
@@ -330,3 +366,7 @@ export function getPrimitiveTypeOf(tv: TypeValue): string | undefined {
|
|
|
330
366
|
if (tv.kind === "instance") return "object";
|
|
331
367
|
return undefined;
|
|
332
368
|
}
|
|
369
|
+
|
|
370
|
+
export function getRefinedBase(tv: TypeValue): TypeValue {
|
|
371
|
+
return tv.kind === "refined" ? getRefinedBase(tv.base) : tv;
|
|
372
|
+
}
|