functionalscript 0.41.0 → 0.43.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/fjs/bnf/data/module.f.d.ts +2 -2
- package/fjs/bnf/ll1/module.f.d.ts +1 -1
- package/fjs/bnf/module.f.d.ts +1 -1
- package/fjs/cas/evo/module.f.d.ts +1 -1
- package/fjs/ci/config/module.f.d.ts +3 -3
- package/fjs/ci/config/module.f.js +3 -3
- package/fjs/ci/deno/module.f.d.ts +8 -0
- package/fjs/ci/deno/module.f.js +9 -1
- package/fjs/ci/deno/proof.f.d.ts +5 -0
- package/fjs/ci/deno/proof.f.js +26 -0
- package/fjs/ci/proof.f.js +3 -2
- package/fjs/dev/module.f.d.ts +16 -5
- package/fjs/dev/module.f.js +38 -7
- package/fjs/dev/package_json/module.f.js +7 -4
- package/fjs/dev/proof.f.js +10 -1
- package/fjs/djs/parser/module.f.js +13 -3
- package/fjs/djs/parser/proof.f.d.ts +1 -0
- package/fjs/djs/parser/proof.f.js +26 -0
- package/fjs/djs/tokenizer/proof.f.js +11 -4
- package/fjs/effects/node/module.f.d.ts +2 -2
- package/fjs/emergent_testing/proof.f.js +20 -3
- package/fjs/fsm/module.f.d.ts +1 -1
- package/fjs/js/tokenizer/proof.f.js +7 -0
- package/fjs/mcp/cas/module.f.js +4 -1
- package/fjs/mcp/evo/proof.f.js +6 -1
- package/fjs/mcp/proof.f.js +37 -18
- package/fjs/media/html/module.f.d.ts +1 -1
- package/fjs/media/json/module.f.d.ts +15 -1
- package/fjs/media/json/module.f.js +17 -1
- package/fjs/media/json/parser/module.f.js +13 -3
- package/fjs/media/json/parser/proof.f.d.ts +1 -0
- package/fjs/media/json/parser/proof.f.js +37 -1
- package/fjs/media/json/proof.f.d.ts +4 -0
- package/fjs/media/json/proof.f.js +14 -2
- package/fjs/media/json/serializer/module.f.d.ts +4 -0
- package/fjs/media/json/serializer/module.f.js +39 -2
- package/fjs/media/json/serializer/proof.f.js +32 -8
- package/fjs/media/module.f.d.ts +92 -10
- package/fjs/media/module.f.js +73 -8
- package/fjs/media/proof.f.d.ts +10 -0
- package/fjs/media/proof.f.js +79 -10
- package/fjs/media/revision/module.f.d.ts +9 -1
- package/fjs/media/revision/module.f.js +14 -5
- package/fjs/protocol/mcp/stdio/module.f.js +2 -5
- package/fjs/text/utf16/module.f.d.ts +24 -0
- package/fjs/text/utf16/module.f.js +24 -0
- package/fjs/text/utf16/proof.f.d.ts +1 -0
- package/fjs/text/utf16/proof.f.js +10 -1
- package/fjs/types/object/module.f.d.ts +31 -13
- package/fjs/types/object/module.f.js +4 -3
- package/fjs/types/object/proof.f.d.ts +0 -14
- package/fjs/types/object/proof.f.js +0 -4
- package/fjs/types/rtti/common/module.f.d.ts +2 -2
- package/fjs/types/rtti/module.f.d.ts +1 -1
- package/fjs/types/rtti/ts/module.f.d.ts +1 -1
- package/fjs/types/rtti/validate/proof.f.d.ts +2 -0
- package/fjs/types/rtti/validate/proof.f.js +58 -0
- package/nanvm-lib/tests/proof.f.d.ts +9 -0
- package/nanvm-lib/tests/proof.f.js +58 -7
- package/package.json +2 -2
package/fjs/media/module.f.js
CHANGED
|
@@ -1,13 +1,72 @@
|
|
|
1
1
|
import { fromVec } from '../text/utf8/module.f.js';
|
|
2
2
|
import { detectVec } from './type/module.f.js';
|
|
3
|
-
import {
|
|
4
|
-
import { assertNotNullish } from '../asserts/module.f.js';
|
|
3
|
+
import { parse } from './json/module.f.js';
|
|
4
|
+
import { assert, assertNotNullish } from '../asserts/module.f.js';
|
|
5
|
+
import { validate } from '../types/rtti/validate/module.f.js';
|
|
6
|
+
/** The default refinement: structural validation alone decides the match. */
|
|
7
|
+
const always = () => true;
|
|
8
|
+
/** Structural validation followed by the dialect's own refinement. */
|
|
9
|
+
const matchWith = (v) => (extraValidate) => (u) => {
|
|
10
|
+
const [tag, value] = v(u);
|
|
11
|
+
return tag === 'ok' && extraValidate(value);
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Registers a dialect for detection: its rtti schema, plus whatever rtti can't
|
|
15
|
+
* say about it.
|
|
16
|
+
*
|
|
17
|
+
* The dialect name is read out of the schema's own `dialect` member — that
|
|
18
|
+
* member is a string const in a dialect schema, which is what makes the schema
|
|
19
|
+
* self-discriminating, so a separate name alongside it would be a second copy
|
|
20
|
+
* that can disagree with the first. It follows that no registration can claim
|
|
21
|
+
* one dialect while validating another's blobs. The media type is likewise
|
|
22
|
+
* derived rather than supplied: `application/${dialect}+json`, the mechanical
|
|
23
|
+
* derivation `fjs/media/revision/README.md` documents.
|
|
24
|
+
*
|
|
25
|
+
* `extraValidate` runs *after* structural validation, on the dialect's own
|
|
26
|
+
* decoded type, and closes the gap rtti leaves — "this string is
|
|
27
|
+
* cbase32-decodable", "this number is a non-negative safe integer". It returns
|
|
28
|
+
* `boolean`, so it has no error channel and nothing to report but yes or no,
|
|
29
|
+
* and it never sees the raw bytes: parsing and the schema walk stay with the
|
|
30
|
+
* detector. Omitting it gets classification (what a blob claims to be and
|
|
31
|
+
* structurally looks like); supplying it gets detection as strict as the
|
|
32
|
+
* dialect's own decoder.
|
|
33
|
+
*
|
|
34
|
+
* **The name is neither grammar-checked nor allowlisted.** A schema saying
|
|
35
|
+
* `dialect: 'foo'` yields `application/foo+json`, and that is intended:
|
|
36
|
+
* `vnd.fjs.*` is this repo's convention for its *own* formats, not a constraint
|
|
37
|
+
* on what a caller may detect — another vendor's `vnd.*` blob, or a widely used
|
|
38
|
+
* name not under `vnd.` at all, is a legitimate thing to register. The name is
|
|
39
|
+
* not attacker-controlled either: it comes from a schema a programmer wrote and
|
|
40
|
+
* passed here, never from the blob being classified, so no untrusted string
|
|
41
|
+
* reaches `mime_type` along this path. (A detector that read the name *out of*
|
|
42
|
+
* a blob would need the RFC 6838 grammar check and an allowlist; this one does
|
|
43
|
+
* not.) Registering a name that is not a valid RFC 6838 restricted-name yields
|
|
44
|
+
* a malformed media type in the registrant's own results, and nowhere else.
|
|
45
|
+
*
|
|
46
|
+
* The constraint is `Struct` — every member must be a real rtti `Type`, which
|
|
47
|
+
* is what rejects a member like `() => 42` at compile time (rtti would read it
|
|
48
|
+
* as a thunk and `match` would *throw* on the first blob rather than return
|
|
49
|
+
* `false`). TypeScript cannot also require a direct string `dialect` member
|
|
50
|
+
* under that constraint, so that half is asserted here instead: loudly, once,
|
|
51
|
+
* when the entry is constructed. A thunk-form `dialect`
|
|
52
|
+
* (`() => ['const', 'x']`) is a perfectly valid rtti schema, it just is not
|
|
53
|
+
* registerable — write the string directly, as `revisionSchema` does.
|
|
54
|
+
*/
|
|
55
|
+
export const dialectEntry = (type, extraValidate = always) => {
|
|
56
|
+
const { dialect } = type;
|
|
57
|
+
assert(typeof dialect === 'string', 'dialectEntry: schema has no direct string `dialect` member');
|
|
58
|
+
return { dialect, match: matchWith(validate(type))(extraValidate) };
|
|
59
|
+
};
|
|
5
60
|
/**
|
|
6
|
-
* Classifies a whole buffered `Vec`, the same
|
|
7
|
-
* type }` shape as `fjs/media/type`
|
|
8
|
-
* recognized ahead of the plain
|
|
61
|
+
* Classifies a whole buffered `Vec` against `dialects`, returning the same
|
|
62
|
+
* three-way `{ length, mime_type, type }` shape as `fjs/media/type`
|
|
63
|
+
* `detectVec`, but with dialect-tagged JSON recognized ahead of the plain
|
|
64
|
+
* `text/plain` fallback. The first entry whose `match` accepts the parsed value
|
|
65
|
+
* wins; entries that overlap are the registrant's own business, since matching
|
|
66
|
+
* `dialect` as an exact literal already makes structural validation reject
|
|
67
|
+
* every other dialect's blob.
|
|
9
68
|
*/
|
|
10
|
-
export const detect = (bytes) => {
|
|
69
|
+
export const detect = (dialects) => (bytes) => {
|
|
11
70
|
const base = detectVec(bytes);
|
|
12
71
|
// Only whole-blob-valid UTF-8 text can possibly be JSON; a magic-byte hit
|
|
13
72
|
// or binary fallback is never a dialect match.
|
|
@@ -18,6 +77,12 @@ export const detect = (bytes) => {
|
|
|
18
77
|
// whole-blob-valid UTF-8 — the same two conditions `fromVec` checks, via the
|
|
19
78
|
// same decoder — so `fromVec` cannot return `null` here.
|
|
20
79
|
const text = assertNotNullish(fromVec(bytes), 'detect: type text implies fromVec succeeds');
|
|
21
|
-
const [tag] =
|
|
22
|
-
|
|
80
|
+
const [tag, value] = parse(text);
|
|
81
|
+
if (tag === 'error') {
|
|
82
|
+
return base;
|
|
83
|
+
}
|
|
84
|
+
const matched = dialects.find(({ match }) => match(value));
|
|
85
|
+
return matched === undefined
|
|
86
|
+
? base
|
|
87
|
+
: { ...base, mime_type: `application/${matched.dialect}+json` };
|
|
23
88
|
};
|
package/fjs/media/proof.f.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
+
import { type DialectEntry } from './module.f.ts';
|
|
1
2
|
export declare const proof: {
|
|
2
3
|
validRevision: () => void;
|
|
3
4
|
keyOrderIndependent: () => void;
|
|
4
5
|
invalidRevisionFallsThrough: () => void;
|
|
6
|
+
nonHashSnapshotFallsThrough: () => void;
|
|
7
|
+
secondDialect: () => void;
|
|
8
|
+
nonVndDialectName: () => void;
|
|
9
|
+
firstMatchWins: () => void;
|
|
10
|
+
noDialects: () => void;
|
|
5
11
|
ordinaryJsonFallsThrough: () => void;
|
|
6
12
|
plainTextFallsThrough: () => void;
|
|
7
13
|
binaryFallsThrough: () => void;
|
|
8
14
|
largeNonJsonWithinBound: () => void;
|
|
15
|
+
throw: {
|
|
16
|
+
thunkDialectMember: () => DialectEntry;
|
|
17
|
+
noDialectMember: () => DialectEntry;
|
|
18
|
+
};
|
|
9
19
|
};
|
package/fjs/media/proof.f.js
CHANGED
|
@@ -1,14 +1,37 @@
|
|
|
1
1
|
import { assertEq } from '../asserts/module.f.js';
|
|
2
2
|
import { msb, u8ListToVec, repeat, vec8 } from '../types/bit_vec/module.f.js';
|
|
3
|
-
import { detect } from './module.f.js';
|
|
4
|
-
import { dialect } from './revision/module.f.js';
|
|
3
|
+
import { detect, dialectEntry } from './module.f.js';
|
|
4
|
+
import { dialect, revisionDialect } from './revision/module.f.js';
|
|
5
|
+
import { number, string } from '../types/rtti/module.f.js';
|
|
5
6
|
// All test strings here are ASCII, so char code === UTF-8 byte value.
|
|
6
7
|
const utf8Bytes = (s) => u8ListToVec(msb)([...s].map(c => c.charCodeAt(0)));
|
|
7
8
|
const revisionJson = `{"dialect":"${dialect}","subject":"8","parents":[],"snapshot":"8","generation":0}`;
|
|
9
|
+
const dialects = [revisionDialect];
|
|
10
|
+
const detectRevision = detect(dialects);
|
|
11
|
+
/**
|
|
12
|
+
* A second dialect following the same `vnd.fjs.<name>` convention, registered
|
|
13
|
+
* with no refinement: structure alone decides the match.
|
|
14
|
+
*/
|
|
15
|
+
const noteSchema = {
|
|
16
|
+
dialect: 'vnd.fjs.note',
|
|
17
|
+
text: string,
|
|
18
|
+
};
|
|
19
|
+
const noteDialect = dialectEntry(noteSchema);
|
|
20
|
+
/** A dialect name outside `vnd.fjs.*` — registerable, and detected as itself. */
|
|
21
|
+
const gadgetSchema = {
|
|
22
|
+
dialect: 'application.gadget',
|
|
23
|
+
size: number,
|
|
24
|
+
};
|
|
25
|
+
const gadgetDialect = dialectEntry(gadgetSchema);
|
|
26
|
+
/**
|
|
27
|
+
* `DialectEntry` is deliberately not opaque, so overlapping entries — which a
|
|
28
|
+
* self-discriminating schema can't produce on its own — can be stated directly.
|
|
29
|
+
*/
|
|
30
|
+
const anyEntry = (name) => ({ dialect: name, match: () => true });
|
|
8
31
|
export const proof = {
|
|
9
32
|
// A valid revision blob is recognized and reported under its derived media type.
|
|
10
33
|
validRevision: () => {
|
|
11
|
-
const m =
|
|
34
|
+
const m = detectRevision(utf8Bytes(revisionJson));
|
|
12
35
|
assertEq(m.type, 'text');
|
|
13
36
|
assertEq(m.mime_type, 'application/vnd.fjs.revision+json');
|
|
14
37
|
},
|
|
@@ -16,7 +39,7 @@ export const proof = {
|
|
|
16
39
|
// not first must still be detected.
|
|
17
40
|
keyOrderIndependent: () => {
|
|
18
41
|
const text = `{"parents":[],"subject":"8","dialect":"${dialect}","snapshot":"8","generation":0}`;
|
|
19
|
-
const m =
|
|
42
|
+
const m = detectRevision(utf8Bytes(text));
|
|
20
43
|
assertEq(m.mime_type, 'application/vnd.fjs.revision+json');
|
|
21
44
|
},
|
|
22
45
|
// A revision missing a required field (here `snapshot` and `generation`)
|
|
@@ -24,25 +47,63 @@ export const proof = {
|
|
|
24
47
|
// classification.
|
|
25
48
|
invalidRevisionFallsThrough: () => {
|
|
26
49
|
const text = `{"dialect":"${dialect}","subject":"8","parents":["8","r"]}`;
|
|
27
|
-
const m =
|
|
50
|
+
const m = detectRevision(utf8Bytes(text));
|
|
51
|
+
assertEq(m.type, 'text');
|
|
52
|
+
assertEq(m.mime_type, 'text/plain');
|
|
53
|
+
},
|
|
54
|
+
// The `extraValidate` path: structurally a revision, but `snapshot` is not
|
|
55
|
+
// a cbase32 hash, so `revisionDialect`'s refinement rejects it and the
|
|
56
|
+
// verdict stays `text/plain` — exactly what `decodeText` would say.
|
|
57
|
+
nonHashSnapshotFallsThrough: () => {
|
|
58
|
+
const text = `{"dialect":"${dialect}","subject":"8","parents":[],"snapshot":"not a hash","generation":0}`;
|
|
59
|
+
const m = detectRevision(utf8Bytes(text));
|
|
60
|
+
assertEq(m.type, 'text');
|
|
61
|
+
assertEq(m.mime_type, 'text/plain');
|
|
62
|
+
},
|
|
63
|
+
// A second dialect, registered by the caller, is recognized alongside the
|
|
64
|
+
// first — and an entry with no refinement matches on structure alone.
|
|
65
|
+
secondDialect: () => {
|
|
66
|
+
const d = detect([revisionDialect, noteDialect]);
|
|
67
|
+
assertEq(d(utf8Bytes('{"dialect":"vnd.fjs.note","text":"hi"}')).mime_type, 'application/vnd.fjs.note+json');
|
|
68
|
+
assertEq(d(utf8Bytes(revisionJson)).mime_type, 'application/vnd.fjs.revision+json');
|
|
69
|
+
// Same tag, wrong shape: no entry matches.
|
|
70
|
+
assertEq(d(utf8Bytes('{"dialect":"vnd.fjs.note","text":42}')).mime_type, 'text/plain');
|
|
71
|
+
},
|
|
72
|
+
// The name is neither grammar-checked nor allowlisted: a dialect outside
|
|
73
|
+
// `vnd.fjs.*` yields its own derived media type.
|
|
74
|
+
nonVndDialectName: () => {
|
|
75
|
+
const m = detect([gadgetDialect])(utf8Bytes('{"dialect":"application.gadget","size":3}'));
|
|
76
|
+
assertEq(m.mime_type, 'application/application.gadget+json');
|
|
77
|
+
},
|
|
78
|
+
// Entries are tried in order and the first match wins.
|
|
79
|
+
firstMatchWins: () => {
|
|
80
|
+
const bytes = utf8Bytes('{}');
|
|
81
|
+
assertEq(detect([anyEntry('vnd.fjs.a'), anyEntry('vnd.fjs.b')])(bytes).mime_type, 'application/vnd.fjs.a+json');
|
|
82
|
+
assertEq(detect([anyEntry('vnd.fjs.b'), anyEntry('vnd.fjs.a')])(bytes).mime_type, 'application/vnd.fjs.b+json');
|
|
83
|
+
},
|
|
84
|
+
// An empty registry recognizes nothing; every blob keeps the
|
|
85
|
+
// `fjs/media/type` verdict.
|
|
86
|
+
noDialects: () => {
|
|
87
|
+
const m = detect([])(utf8Bytes(revisionJson));
|
|
28
88
|
assertEq(m.type, 'text');
|
|
29
89
|
assertEq(m.mime_type, 'text/plain');
|
|
30
90
|
},
|
|
31
91
|
// Ordinary JSON with no `dialect` field at all falls through unchanged.
|
|
32
92
|
ordinaryJsonFallsThrough: () => {
|
|
33
|
-
const m =
|
|
93
|
+
const m = detectRevision(utf8Bytes('{"hello":"world"}'));
|
|
34
94
|
assertEq(m.type, 'text');
|
|
35
95
|
assertEq(m.mime_type, 'text/plain');
|
|
36
96
|
},
|
|
37
|
-
// Plain (non-JSON) text is unaffected
|
|
97
|
+
// Plain (non-JSON) text is unaffected: the parse fails and the
|
|
98
|
+
// `fjs/media/type` verdict stands.
|
|
38
99
|
plainTextFallsThrough: () => {
|
|
39
|
-
const m =
|
|
100
|
+
const m = detectRevision(utf8Bytes('hello'));
|
|
40
101
|
assertEq(m.type, 'text');
|
|
41
102
|
assertEq(m.mime_type, 'text/plain');
|
|
42
103
|
},
|
|
43
104
|
// Binary content (magic-byte hit) is unaffected by dialect detection.
|
|
44
105
|
binaryFallsThrough: () => {
|
|
45
|
-
const m =
|
|
106
|
+
const m = detectRevision(u8ListToVec(msb)([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
|
46
107
|
assertEq(m.type, 'base64');
|
|
47
108
|
assertEq(m.mime_type, 'image/png');
|
|
48
109
|
},
|
|
@@ -51,8 +112,16 @@ export const proof = {
|
|
|
51
112
|
// non-JSON content within that bound.
|
|
52
113
|
largeNonJsonWithinBound: () => {
|
|
53
114
|
const a = repeat(1000n)(vec8(0x61n)); // 1,000 bytes of 'a'
|
|
54
|
-
const m =
|
|
115
|
+
const m = detectRevision(a);
|
|
55
116
|
assertEq(m.type, 'text');
|
|
56
117
|
assertEq(m.mime_type, 'text/plain');
|
|
57
118
|
},
|
|
119
|
+
throw: {
|
|
120
|
+
// The `dialect` member must be a direct string const. A thunk-form one
|
|
121
|
+
// is a valid rtti schema but is not registerable, and `dialectEntry`
|
|
122
|
+
// says so at registration rather than per blob.
|
|
123
|
+
thunkDialectMember: () => dialectEntry({ dialect: string }),
|
|
124
|
+
// A schema with no `dialect` member at all is likewise refused.
|
|
125
|
+
noDialectMember: () => dialectEntry({ text: string }),
|
|
126
|
+
},
|
|
58
127
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type ValidationError } from '../../types/rtti/validate/module.f.ts';
|
|
2
2
|
import type { Ts } from '../../types/rtti/ts/module.f.ts';
|
|
3
|
-
import type
|
|
3
|
+
import { type Unknown } from '../json/module.f.ts';
|
|
4
4
|
import { type Result } from '../../types/result/module.f.ts';
|
|
5
|
+
import { type DialectEntry } from '../module.f.ts';
|
|
5
6
|
/**
|
|
6
7
|
* Format tag: names the dialect of this BLOB. The media type it is served
|
|
7
8
|
* with is derived mechanically: `application/` + `dialect` + `+json`.
|
|
@@ -76,3 +77,10 @@ export declare const validate: (value: Unknown) => Result<Revision, RevisionErro
|
|
|
76
77
|
* satisfies the schema is a revision, regardless of key order or whitespace.
|
|
77
78
|
*/
|
|
78
79
|
export declare const decodeText: (text: string) => Result<Revision, RevisionError>;
|
|
80
|
+
/**
|
|
81
|
+
* This dialect as a registry entry for `fjs/media`'s `detect`. It carries the
|
|
82
|
+
* semantic checks too, so a blob is detected as `vnd.fjs.revision` exactly when
|
|
83
|
+
* {@link decodeText} would accept it — a structurally valid revision whose
|
|
84
|
+
* `snapshot` is not a cbase32 hash is not one.
|
|
85
|
+
*/
|
|
86
|
+
export declare const revisionDialect: DialectEntry;
|
|
@@ -16,11 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { array, number, option, string } from '../../types/rtti/module.f.js';
|
|
18
18
|
import { validate as rttiValidate } from '../../types/rtti/validate/module.f.js';
|
|
19
|
-
import { parse as
|
|
20
|
-
import { tokenize as jsonTokenize } from '../json/tokenizer/module.f.js';
|
|
21
|
-
import { stringToList } from '../../text/utf16/module.f.js';
|
|
19
|
+
import { parse as parseJson } from '../json/module.f.js';
|
|
22
20
|
import { cBase32ToVec } from '../../basen/cbase32/module.f.js';
|
|
23
21
|
import { error, ok } from '../../types/result/module.f.js';
|
|
22
|
+
import { dialectEntry } from '../module.f.js';
|
|
24
23
|
/**
|
|
25
24
|
* Format tag: names the dialect of this BLOB. The media type it is served
|
|
26
25
|
* with is derived mechanically: `application/` + `dialect` + `+json`.
|
|
@@ -106,8 +105,6 @@ export const validate = (value) => {
|
|
|
106
105
|
}
|
|
107
106
|
return checkReferences(v);
|
|
108
107
|
};
|
|
109
|
-
/** Parses `text` as JSON without relying on `JSON.parse` (the shared `fjs/media/json` parser/tokenizer). */
|
|
110
|
-
const parseJson = (text) => jsonParse(jsonTokenize(stringToList(text)));
|
|
111
108
|
/**
|
|
112
109
|
* Decodes `text` as a `revision` BLOB: JSON-parses it, then validates it per
|
|
113
110
|
* {@link validate}. Detection is semantic, not syntactic — any JSON that
|
|
@@ -120,3 +117,15 @@ export const decodeText = (text) => {
|
|
|
120
117
|
}
|
|
121
118
|
return validate(v);
|
|
122
119
|
};
|
|
120
|
+
/** {@link checkReferences} as the `boolean` refinement a {@link DialectEntry} takes. */
|
|
121
|
+
const isValidRevision = (r) => {
|
|
122
|
+
const [tag] = checkReferences(r);
|
|
123
|
+
return tag === 'ok';
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* This dialect as a registry entry for `fjs/media`'s `detect`. It carries the
|
|
127
|
+
* semantic checks too, so a blob is detected as `vnd.fjs.revision` exactly when
|
|
128
|
+
* {@link decodeText} would accept it — a structurally valid revision whose
|
|
129
|
+
* `snapshot` is not a cbase32 hash is not one.
|
|
130
|
+
*/
|
|
131
|
+
export const revisionDialect = dialectEntry(revisionSchema, isValidRevision);
|
|
@@ -32,10 +32,7 @@
|
|
|
32
32
|
import { pure, step } from '../../../effects/module.f.js';
|
|
33
33
|
import { readLine, write } from '../../../effects/node/module.f.js';
|
|
34
34
|
import { tryUtf8 } from '../../../text/module.f.js';
|
|
35
|
-
import {
|
|
36
|
-
import { stringify } from '../../../media/json/module.f.js';
|
|
37
|
-
import { tokenize } from '../../../media/json/tokenizer/module.f.js';
|
|
38
|
-
import { parse } from '../../../media/json/parser/module.f.js';
|
|
35
|
+
import { parse, stringify } from '../../../media/json/module.f.js';
|
|
39
36
|
import { sort } from '../../../types/object/module.f.js';
|
|
40
37
|
import { internalError, jsonrpc, parseError } from '../../json_rpc/module.f.js';
|
|
41
38
|
import { error, ok } from '../../../types/result/module.f.js';
|
|
@@ -61,7 +58,7 @@ export const stdioTransport = (handler) => step(readLine('stdin'), line => line
|
|
|
61
58
|
? pure(undefined)
|
|
62
59
|
: handleLine(handler)(line));
|
|
63
60
|
const handleLine = (handler) => (line) => {
|
|
64
|
-
const [t, value] = parse(
|
|
61
|
+
const [t, value] = parse(line);
|
|
65
62
|
return step(t === 'error'
|
|
66
63
|
? writeResponse(parseErrorResponse)
|
|
67
64
|
: step(handler(value), resp => resp === null
|
|
@@ -135,3 +135,27 @@ export declare const listToString: (input: List<U16>) => string;
|
|
|
135
135
|
* ```
|
|
136
136
|
*/
|
|
137
137
|
export declare const codePointListToString: (input: List<CodePoint>) => string;
|
|
138
|
+
/**
|
|
139
|
+
* Converts a single Unicode code point to a string — the scalar counterpart of
|
|
140
|
+
* {@link codePointListToString}, for callers that hold one code point rather
|
|
141
|
+
* than a list of them and would otherwise wrap it in a single-element list.
|
|
142
|
+
*
|
|
143
|
+
* Input outside the assignable range — a surrogate, a value above
|
|
144
|
+
* `0x10FFFF`, or a negative one — is truncated to its low 16 bits and encoded
|
|
145
|
+
* as that code unit, rather than rejected. This matches
|
|
146
|
+
* {@link codePointListToString}, which reports malformed input through the
|
|
147
|
+
* decoder's `errorMask` on the way *in* rather than through a failure on the
|
|
148
|
+
* way out.
|
|
149
|
+
*
|
|
150
|
+
* @param codePoint - A Unicode code point (`CodePoint`).
|
|
151
|
+
* @returns The one- or two-code-unit string encoding it.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* codePointToString(0x48) // 'H'
|
|
157
|
+
* codePointToString(0x1F600) // the 😀 surrogate pair
|
|
158
|
+
* codePointToString(0x110000) // '\u0000' — out of range, low 16 bits kept
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export declare const codePointToString: (codePoint: CodePoint) => string;
|
|
@@ -285,3 +285,27 @@ export const listToString = fn(map(String.fromCharCode))
|
|
|
285
285
|
* ```
|
|
286
286
|
*/
|
|
287
287
|
export const codePointListToString = (input) => listToString(fromCodePointList(input));
|
|
288
|
+
/**
|
|
289
|
+
* Converts a single Unicode code point to a string — the scalar counterpart of
|
|
290
|
+
* {@link codePointListToString}, for callers that hold one code point rather
|
|
291
|
+
* than a list of them and would otherwise wrap it in a single-element list.
|
|
292
|
+
*
|
|
293
|
+
* Input outside the assignable range — a surrogate, a value above
|
|
294
|
+
* `0x10FFFF`, or a negative one — is truncated to its low 16 bits and encoded
|
|
295
|
+
* as that code unit, rather than rejected. This matches
|
|
296
|
+
* {@link codePointListToString}, which reports malformed input through the
|
|
297
|
+
* decoder's `errorMask` on the way *in* rather than through a failure on the
|
|
298
|
+
* way out.
|
|
299
|
+
*
|
|
300
|
+
* @param codePoint - A Unicode code point (`CodePoint`).
|
|
301
|
+
* @returns The one- or two-code-unit string encoding it.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
*
|
|
305
|
+
* ```ts
|
|
306
|
+
* codePointToString(0x48) // 'H'
|
|
307
|
+
* codePointToString(0x1F600) // the 😀 surrogate pair
|
|
308
|
+
* codePointToString(0x110000) // '\u0000' — out of range, low 16 bits kept
|
|
309
|
+
* ```
|
|
310
|
+
*/
|
|
311
|
+
export const codePointToString = (codePoint) => String.fromCharCode(...codePointToUtf16(codePoint));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toCodePointList, fromCodePointList, stringToList, listToString, stringToCodePointList, codePointListToString } from './module.f.js';
|
|
1
|
+
import { toCodePointList, fromCodePointList, stringToList, listToString, stringToCodePointList, codePointListToString, codePointToString } from './module.f.js';
|
|
2
2
|
import { stringify as jsonStringify } from '../../media/json/module.f.js';
|
|
3
3
|
import { sort } from '../../types/object/module.f.js';
|
|
4
4
|
import { toArray } from '../../types/list/module.f.js';
|
|
@@ -131,5 +131,14 @@ export const proof = {
|
|
|
131
131
|
const codePoints = [0x48, 0x65, 0x6C, 0x6C, 0x6F];
|
|
132
132
|
const outputString = codePointListToString(codePoints);
|
|
133
133
|
}
|
|
134
|
+
],
|
|
135
|
+
codePointToString: [
|
|
136
|
+
() => { assertEq(codePointToString(0x48), 'H'); },
|
|
137
|
+
// supplementary plane: one code point, two code units
|
|
138
|
+
() => { assertEq(codePointToString(0x1f600), '😀'); },
|
|
139
|
+
// a surrogate is not a code point; it round-trips as its own code unit
|
|
140
|
+
() => { assertEq(codePointToString(0xd800), '\ud800'); },
|
|
141
|
+
// above the maximum code point, the low 16 bits are kept
|
|
142
|
+
() => { assertEq(codePointToString(0x110000), '\u0000'); },
|
|
134
143
|
]
|
|
135
144
|
};
|
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { type List } from '../list/module.f.ts';
|
|
2
2
|
import { type Nullable } from '../nullable/module.f.ts';
|
|
3
3
|
import { type OrderedMap } from '../ordered_map/module.f.ts';
|
|
4
|
-
|
|
4
|
+
/** A record over the keys of `K`, each value possibly missing at runtime. */
|
|
5
|
+
export type OptionalMap<K extends string, T> = {
|
|
6
|
+
readonly [k in K]?: T;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* A record over the keys of `K`, each value required.
|
|
10
|
+
*
|
|
11
|
+
* `K` has to be a finite union of string literals: `RequiredMap<string, T>` is
|
|
12
|
+
* `never`, because no object can carry every string as a key. Use `StringMap<T>`
|
|
13
|
+
* for an open key set — its values are optional, which is what such a key set
|
|
14
|
+
* means at runtime.
|
|
15
|
+
*
|
|
16
|
+
* There is no known way to ask TypeScript whether `K` is finite, so the guard
|
|
17
|
+
* approximates it with `string extends K`, which holds exactly when `K` is
|
|
18
|
+
* `string`. Other infinite key sets are not caught: a template literal such as
|
|
19
|
+
* `x-${string}` yields a template index signature instead of `never`, and its
|
|
20
|
+
* reads are typed `T` while the runtime value is `undefined`. Keep `K` a union
|
|
21
|
+
* of string literals.
|
|
22
|
+
*/
|
|
23
|
+
export type RequiredMap<K extends string, T> = string extends K ? never : {
|
|
24
|
+
readonly [k in K]: T;
|
|
25
|
+
};
|
|
26
|
+
/** A record with an open key set. Every value can be missing at runtime. */
|
|
27
|
+
export type StringMap<T> = OptionalMap<string, T>;
|
|
5
28
|
export type Entry<T> = readonly [string, T];
|
|
6
|
-
export declare const at: (name: string) => <T>(object:
|
|
29
|
+
export declare const at: (name: string) => <T>(object: StringMap<T>) => Nullable<Exclude<T, undefined>>;
|
|
7
30
|
export declare const sort: <T>(e: List<Entry<T>>) => List<Entry<T>>;
|
|
8
|
-
export declare const fromEntries: <T>(e: List<Entry<T>>) =>
|
|
9
|
-
export declare const fromMap: <T>(m: OrderedMap<T>) =>
|
|
31
|
+
export declare const fromEntries: <T>(e: List<Entry<T>>) => StringMap<T>;
|
|
32
|
+
export declare const fromMap: <T>(m: OrderedMap<T>) => StringMap<T>;
|
|
10
33
|
/**
|
|
11
34
|
* A set of objects with a single key.
|
|
12
35
|
*
|
|
@@ -14,7 +37,7 @@ export declare const fromMap: <T>(m: OrderedMap<T>) => Map<T>;
|
|
|
14
37
|
* https://stackoverflow.com/questions/57571664/typescript-type-for-an-object-with-only-one-key-no-union-type-allowed-as-a-key
|
|
15
38
|
*/
|
|
16
39
|
export type OneKey<K extends string, V> = {
|
|
17
|
-
[P in K]: (
|
|
40
|
+
[P in K]: (RequiredMap<P, V> & OptionalMap<Exclude<K, P>, never>) extends infer O ? {
|
|
18
41
|
[Q in keyof O]: O[Q];
|
|
19
42
|
} : never;
|
|
20
43
|
}[K];
|
|
@@ -24,13 +47,8 @@ export type OneKey<K extends string, V> = {
|
|
|
24
47
|
export type NotUnion<T, U = T> = T extends unknown ? [
|
|
25
48
|
U
|
|
26
49
|
] extends [T] ? T : never : never;
|
|
27
|
-
export type SingleProperty<T extends StringMap<
|
|
50
|
+
export type SingleProperty<T extends StringMap<never>> = keyof T extends NotUnion<keyof T> ? T : never;
|
|
28
51
|
export declare const isObject: (value: unknown) => value is { readonly [k in string]: unknown; };
|
|
29
52
|
/** Returns only the defined (non-undefined) values of a partial record. */
|
|
30
|
-
export declare const definedValues: <T>(map: StringMap<
|
|
31
|
-
export
|
|
32
|
-
readonly [k in string]?: T;
|
|
33
|
-
} : {
|
|
34
|
-
readonly [k in K]: T;
|
|
35
|
-
};
|
|
36
|
-
export declare const definedEntries: <T>(cmd: StringMap<string, Exclude<T, undefined>>) => readonly (readonly [string, Exclude<T, undefined>])[];
|
|
53
|
+
export declare const definedValues: <T>(map: StringMap<Exclude<T, undefined>>) => readonly Exclude<T, undefined>[];
|
|
54
|
+
export declare const definedEntries: <T>(cmd: StringMap<Exclude<T, undefined>>) => readonly (readonly [string, Exclude<T, undefined>])[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Plain-object helpers and types: `
|
|
3
|
-
*
|
|
4
|
-
* `OneKey`/`SingleProperty`/`NotUnion`
|
|
2
|
+
* Plain-object helpers and types: the `OptionalMap`/`RequiredMap`/`StringMap`
|
|
3
|
+
* record shapes and `Entry<T>`, safe property lookup via `at`, conversions
|
|
4
|
+
* between entries and `OrderedMap`, and the `OneKey`/`SingleProperty`/`NotUnion`
|
|
5
|
+
* utility types.
|
|
5
6
|
*
|
|
6
7
|
* @module
|
|
7
8
|
*/
|
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import type { StringMap } from './module.f.ts';
|
|
2
|
-
type E<A, B> = A extends B ? B extends A ? true : false : false;
|
|
3
|
-
type _InfiniteIsOptional = E<StringMap<string, bigint>, {
|
|
4
|
-
readonly [k in string]?: bigint;
|
|
5
|
-
}>;
|
|
6
|
-
type _FiniteIsRequired = E<StringMap<'a' | 'b', bigint>, {
|
|
7
|
-
readonly a: bigint;
|
|
8
|
-
readonly b: bigint;
|
|
9
|
-
}>;
|
|
10
1
|
export declare const proof: {
|
|
11
|
-
stringMap: {
|
|
12
|
-
infiniteIsOptional: _InfiniteIsOptional;
|
|
13
|
-
finiteIsRequired: _FiniteIsRequired;
|
|
14
|
-
};
|
|
15
2
|
ctor: () => void;
|
|
16
3
|
property: () => void;
|
|
17
4
|
};
|
|
18
|
-
export {};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import { at } from './module.f.js';
|
|
2
2
|
import { assertEq } from '../../asserts/module.f.js';
|
|
3
3
|
export const proof = {
|
|
4
|
-
stringMap: {
|
|
5
|
-
infiniteIsOptional: true,
|
|
6
|
-
finiteIsRequired: true,
|
|
7
|
-
},
|
|
8
4
|
ctor: () => {
|
|
9
5
|
const a = {};
|
|
10
6
|
const value = at('constructor')(a);
|
|
@@ -74,11 +74,11 @@ export type Visitor<R> = {
|
|
|
74
74
|
/** Type guard narrowing `Unknown` to a specific container type `C`. */
|
|
75
75
|
export type IsContainer<C extends Unknown> = (value: Unknown) => value is C;
|
|
76
76
|
/** Maps a `Tag1` to its runtime container type. */
|
|
77
|
-
export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : StringMap<
|
|
77
|
+
export type Container<K extends Tag1> = K extends 'array' ? ReadonlyArray<Unknown> : StringMap<Unknown>;
|
|
78
78
|
/** `IsContainer` guard for arrays, shared by `validate` and `parse`. */
|
|
79
79
|
export declare const isArray: IsContainer<ReadonlyArray<Unknown>>;
|
|
80
80
|
/** `IsContainer` guard for records/structs, shared by `validate` and `parse`. */
|
|
81
|
-
export declare const isObject: IsContainer<StringMap<
|
|
81
|
+
export declare const isObject: IsContainer<StringMap<Unknown>>;
|
|
82
82
|
/**
|
|
83
83
|
* Runs `item` over each `[key, value]` entry, bailing out with the first
|
|
84
84
|
* error, path-prefixed with that entry's key. On success, folds each item's
|
|
@@ -6,7 +6,7 @@ export type Const = null | boolean | number | string | undefined | bigint | {
|
|
|
6
6
|
} | readonly Type[];
|
|
7
7
|
export type ConstObject = Struct | Tuple;
|
|
8
8
|
/** A struct schema: plain object whose values are nested `Type`s. */
|
|
9
|
-
export type Struct = StringMap<
|
|
9
|
+
export type Struct = StringMap<Type>;
|
|
10
10
|
/** A tuple schema: readonly array whose elements are nested `Type`s. */
|
|
11
11
|
export type Tuple = readonly Type[];
|
|
12
12
|
declare const primitive0List: readonly ['bigint', 'boolean', 'number', 'string'];
|
|
@@ -31,7 +31,7 @@ export type Object = {
|
|
|
31
31
|
/** Maps a `Tag0` to its TypeScript type. */
|
|
32
32
|
export type Info0Ts<T extends Tag0> = T extends 'boolean' ? boolean : T extends 'number' ? number : T extends 'string' ? string : T extends 'bigint' ? bigint : T extends 'unknown' ? Unknown : never;
|
|
33
33
|
/** Maps a `Const` schema to its TypeScript type. */
|
|
34
|
-
export type ConstTs<T> = T extends readonly Type[] ? TupleTs<T> : T extends StringMap<
|
|
34
|
+
export type ConstTs<T> = T extends readonly Type[] ? TupleTs<T> : T extends StringMap<Type> ? StructTs<T> : T;
|
|
35
35
|
/** Maps a `Tag1` and inner type to its TypeScript type. */
|
|
36
36
|
export type Info1Ts<K extends Tag1, T extends Type> = K extends 'array' ? ArrayTs<T> : K extends 'record' ? RecordTs<T> : never;
|
|
37
37
|
/** Maps an array schema `T` to `readonly Ts<T>[]`. */
|
|
@@ -288,4 +288,62 @@ export const proof = {
|
|
|
288
288
|
assertError(v({ a: 42 }));
|
|
289
289
|
},
|
|
290
290
|
},
|
|
291
|
+
funcParam: () => {
|
|
292
|
+
const paramSet0 = ['hello', bigint];
|
|
293
|
+
const paramSet1 = ['goodbye', string, 43];
|
|
294
|
+
const paramSet01 = or(paramSet0, paramSet1);
|
|
295
|
+
const v0 = validate(paramSet0);
|
|
296
|
+
const v1 = validate(paramSet1);
|
|
297
|
+
const func = (f0, f1) => (...args) => {
|
|
298
|
+
{
|
|
299
|
+
const [t, r] = v0(args);
|
|
300
|
+
if (t === 'ok') {
|
|
301
|
+
return f0(...r);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
{
|
|
305
|
+
const [t, r] = v1(args);
|
|
306
|
+
if (t === 'ok') {
|
|
307
|
+
return f1(...r);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
throw 'unreachable: args did not match any parameter set';
|
|
311
|
+
};
|
|
312
|
+
const f0 = (a, b) => 42;
|
|
313
|
+
const f1 = (a, b, c) => 13;
|
|
314
|
+
const x = func(f0, f1);
|
|
315
|
+
return () => {
|
|
316
|
+
assertEq(x('hello', 42n), 42);
|
|
317
|
+
assertEq(x('goodbye', 'world', 43), 13);
|
|
318
|
+
};
|
|
319
|
+
},
|
|
320
|
+
funcObj: () => {
|
|
321
|
+
const param0 = { a: 'hello', b: bigint };
|
|
322
|
+
const param1 = { a: 'goodbye', b: string, c: 43 };
|
|
323
|
+
const param01 = or(param0, param1);
|
|
324
|
+
const v0 = validate(param0);
|
|
325
|
+
const v1 = validate(param1);
|
|
326
|
+
const func = (f0, f1) => (args) => {
|
|
327
|
+
{
|
|
328
|
+
const [t, r] = v0(args);
|
|
329
|
+
if (t === 'ok') {
|
|
330
|
+
return f0(r);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
{
|
|
334
|
+
const [t, r] = v1(args);
|
|
335
|
+
if (t === 'ok') {
|
|
336
|
+
return f1(r);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw 'unreachable: args did not match any parameter set';
|
|
340
|
+
};
|
|
341
|
+
const f0 = (args) => Number(args.b) + args.a.length;
|
|
342
|
+
const f1 = (args) => args.c;
|
|
343
|
+
const x = func(f0, f1);
|
|
344
|
+
return () => {
|
|
345
|
+
assertEq(x({ a: 'hello', b: 42n }), 47);
|
|
346
|
+
assertEq(x({ a: 'goodbye', b: 'world', c: 43 }), 43);
|
|
347
|
+
};
|
|
348
|
+
}
|
|
291
349
|
};
|