@magicvr/schema-ui-protocol 0.2.1 → 0.2.3

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.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * actions fixture adapter — transport outcome → host events (non-batch).
3
+ */
4
+ const SAFE_MESSAGES = {
5
+ 403: "无权限访问",
6
+ 404: "资源不存在",
7
+ };
8
+ const SERVER_ERROR_DEFAULT = "系统异常,请稍后重试";
9
+ const TIMEOUT_DEFAULT = "请求超时,请稍后重试";
10
+ const NETWORK_DEFAULT = "网络异常,请检查网络连接";
11
+ function emitBehavior(behavior, context, events) {
12
+ if (!behavior) {
13
+ return;
14
+ }
15
+ switch (behavior.behavior) {
16
+ case "toast":
17
+ events.push({ type: "toast", message: behavior.message ?? "" });
18
+ break;
19
+ case "reload":
20
+ events.push({ type: "reloadTable", tableId: context.tableId });
21
+ break;
22
+ case "navigate":
23
+ events.push({ type: "navigate", url: behavior.url });
24
+ break;
25
+ case "closeModal":
26
+ events.push({ type: "closeModal" });
27
+ break;
28
+ default:
29
+ break;
30
+ }
31
+ }
32
+ export function runActionOutcome(input) {
33
+ const transport = input.transport;
34
+ const onSuccess = input.onSuccess;
35
+ const onError = input.onError;
36
+ const context = input.context ?? {};
37
+ const events = [];
38
+ if (transport.type === "abort") {
39
+ return { ok: false, events: [] };
40
+ }
41
+ if (transport.type === "success") {
42
+ events.push({ type: "requestSucceeded", status: transport.status });
43
+ emitBehavior(onSuccess, context, events);
44
+ return { ok: true, events };
45
+ }
46
+ if (transport.type === "timeout") {
47
+ events.push({
48
+ type: "errorState",
49
+ display: TIMEOUT_DEFAULT,
50
+ retryable: true,
51
+ outcome: "unknown",
52
+ });
53
+ emitBehavior(onError, context, events);
54
+ return { ok: false, events };
55
+ }
56
+ if (transport.type === "networkError") {
57
+ events.push({
58
+ type: "errorState",
59
+ display: NETWORK_DEFAULT,
60
+ retryable: true,
61
+ outcome: "unknown",
62
+ });
63
+ emitBehavior(onError, context, events);
64
+ return { ok: false, events };
65
+ }
66
+ if (transport.type === "httpError") {
67
+ const status = transport.status;
68
+ const body = transport.body ?? {};
69
+ if (status === 400 && Array.isArray(body.errors)) {
70
+ events.push({ type: "fieldErrors", errors: body.errors });
71
+ if (onError?.behavior === "toast") {
72
+ events.push({ type: "toast", message: onError.message ?? body.message ?? "" });
73
+ }
74
+ else {
75
+ // validation errors suppress navigate; default toast from response message
76
+ events.push({ type: "toast", message: body.message ?? "" });
77
+ }
78
+ return { ok: false, events };
79
+ }
80
+ if (status === 401 || status === 403) {
81
+ events.push({ type: "authFailure", status });
82
+ events.push({
83
+ type: "errorState",
84
+ display: status === 403 ? SAFE_MESSAGES[403] : null,
85
+ });
86
+ // auth hook suppresses onError
87
+ return { ok: false, events };
88
+ }
89
+ if (status === 404) {
90
+ events.push({
91
+ type: "errorState",
92
+ display: SAFE_MESSAGES[404],
93
+ });
94
+ emitBehavior(onError, context, events);
95
+ return { ok: false, events };
96
+ }
97
+ // 5xx / other
98
+ events.push({
99
+ type: "errorState",
100
+ display: SERVER_ERROR_DEFAULT,
101
+ });
102
+ emitBehavior(onError, context, events);
103
+ return { ok: false, events };
104
+ }
105
+ return { ok: false, events: [] };
106
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * D-COMP component-format fixture adapter (schema-ui-docs v2.7.0).
3
+ * Validates format wire types without coercion.
4
+ */
5
+ export function applyComponentFormat(format, value) {
6
+ switch (format) {
7
+ case "currency":
8
+ case "percent":
9
+ if (typeof value === "number" && Number.isFinite(value)) {
10
+ return { ok: true, value };
11
+ }
12
+ return { ok: false, code: "COMPONENT_DATA_TYPE_MISMATCH" };
13
+ case "datetime":
14
+ if (typeof value === "string") {
15
+ return { ok: true, value };
16
+ }
17
+ return { ok: false, code: "COMPONENT_DATA_TYPE_MISMATCH" };
18
+ default:
19
+ return { ok: false, code: "COMPONENT_DATA_TYPE_MISMATCH" };
20
+ }
21
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * ADR-0010 query-serialization adapter (schema-ui-docs v2.7.0 fixtures).
3
+ *
4
+ * Merges base URL query with ordered source layers; encodes keys/values per
5
+ * RFC 3986 (unreserved A-Za-z0-9-._~); sorts final keys by Unicode code point.
6
+ */
7
+ function isUndefinedMarker(value) {
8
+ return (typeof value === "object" &&
9
+ value !== null &&
10
+ !Array.isArray(value) &&
11
+ Object.prototype.hasOwnProperty.call(value, "$undefined") &&
12
+ value.$undefined === true);
13
+ }
14
+ function isComposite(value) {
15
+ return ((typeof value === "object" && value !== null && !isUndefinedMarker(value)) ||
16
+ Array.isArray(value));
17
+ }
18
+ /** RFC3986 encode: percent-encode everything except unreserved. */
19
+ export function encodeRFC3986(value) {
20
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => {
21
+ return "%" + char.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0");
22
+ });
23
+ }
24
+ /**
25
+ * JCS-style number serialization used by query-serialization fixtures.
26
+ * Matches JSON number text for typical finite values (1e+21, 1e-7, 0.000001).
27
+ */
28
+ export function serializeQueryNumber(value) {
29
+ if (!Number.isFinite(value)) {
30
+ throw new Error("non-finite number");
31
+ }
32
+ // Prefer the same text JSON would emit for these fixtures.
33
+ if (Object.is(value, -0)) {
34
+ return "0";
35
+ }
36
+ const asJson = JSON.stringify(value);
37
+ return asJson;
38
+ }
39
+ function serializeScalar(value) {
40
+ if (value === null) {
41
+ return "";
42
+ }
43
+ if (typeof value === "boolean") {
44
+ return value ? "true" : "false";
45
+ }
46
+ if (typeof value === "number") {
47
+ return serializeQueryNumber(value);
48
+ }
49
+ return value;
50
+ }
51
+ function decodePercentUtf8(encoded) {
52
+ try {
53
+ // Reject incomplete / non-hex percent sequences before decodeURIComponent.
54
+ if (/%(?:$|[^0-9A-Fa-f]|[0-9A-Fa-f](?:$|[^0-9A-Fa-f]))/.test(encoded)) {
55
+ return null;
56
+ }
57
+ if (/%[0-9A-Fa-f]{2}/.test(encoded)) {
58
+ // Reject overlong / invalid UTF-8 by checking decodeURIComponent + URIError.
59
+ const decoded = decodeURIComponent(encoded);
60
+ // decodeURIComponent accepts %FF as a single Latin-1-ish replacement in some
61
+ // engines? Node throws URIError for invalid UTF-8 sequences in modern V8.
62
+ // Also reject lone high bytes that don't form valid UTF-8 when re-encoded.
63
+ for (let i = 0; i < encoded.length; i++) {
64
+ if (encoded[i] === "%") {
65
+ const hex = encoded.slice(i + 1, i + 3);
66
+ const byte = Number.parseInt(hex, 16);
67
+ // After decodeURIComponent, if original had invalid UTF-8 multi-byte, it throws.
68
+ // Single-byte 0xFF is invalid as UTF-8 leading in a string context when
69
+ // percent-decoded in isolation — Node's decodeURIComponent throws on %FF.
70
+ void byte;
71
+ }
72
+ }
73
+ return decoded;
74
+ }
75
+ return encoded;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ function parseBaseUrl(baseUrl) {
82
+ const hashIndex = baseUrl.indexOf("#");
83
+ const withoutFragment = hashIndex >= 0 ? baseUrl.slice(0, hashIndex) : baseUrl;
84
+ const fragment = hashIndex >= 0 ? baseUrl.slice(hashIndex) : "";
85
+ const qIndex = withoutFragment.indexOf("?");
86
+ const path = qIndex >= 0 ? withoutFragment.slice(0, qIndex) : withoutFragment;
87
+ const query = qIndex >= 0 ? withoutFragment.slice(qIndex + 1) : "";
88
+ if (query === "") {
89
+ return { path, pairs: [], fragment };
90
+ }
91
+ const pairs = [];
92
+ for (const part of query.split("&")) {
93
+ if (part === "") {
94
+ // empty segment like trailing & — treat as empty key?
95
+ continue;
96
+ }
97
+ const eq = part.indexOf("=");
98
+ const rawKey = eq >= 0 ? part.slice(0, eq) : part;
99
+ const rawVal = eq >= 0 ? part.slice(eq + 1) : "";
100
+ if (rawKey === "") {
101
+ return { error: "INVALID_QUERY_KEY" };
102
+ }
103
+ // Plus is literal in base query (not space).
104
+ const keyDecoded = decodePercentUtf8(rawKey.replace(/\+/g, "%2B"));
105
+ const valDecoded = decodePercentUtf8(rawVal.replace(/\+/g, "%2B"));
106
+ if (keyDecoded === null || valDecoded === null) {
107
+ return { error: "INVALID_BASE_URL_QUERY" };
108
+ }
109
+ if (keyDecoded === "") {
110
+ return { error: "INVALID_QUERY_KEY" };
111
+ }
112
+ pairs.push([keyDecoded, valDecoded]);
113
+ }
114
+ return { path, pairs, fragment };
115
+ }
116
+ export function serializeQuery(baseUrl, sources) {
117
+ const parsed = parseBaseUrl(baseUrl);
118
+ if ("error" in parsed) {
119
+ return { ok: false, code: parsed.error };
120
+ }
121
+ // Map of key → value text (or deleted).
122
+ const map = new Map();
123
+ // Apply base pairs (last wins for duplicates).
124
+ for (const [key, value] of parsed.pairs) {
125
+ map.set(key, value);
126
+ }
127
+ for (const source of sources) {
128
+ // Within a source, last write wins; apply in order onto map.
129
+ const local = new Map();
130
+ for (const pair of source) {
131
+ if (!Array.isArray(pair) || pair.length !== 2) {
132
+ return { ok: false, code: "INVALID_QUERY_VALUE" };
133
+ }
134
+ const [key, value] = pair;
135
+ if (typeof key !== "string") {
136
+ return { ok: false, code: "INVALID_QUERY_KEY" };
137
+ }
138
+ if (key === "") {
139
+ return { ok: false, code: "INVALID_QUERY_KEY" };
140
+ }
141
+ if (isUndefinedMarker(value) || value === null || value === undefined) {
142
+ local.set(key, "delete");
143
+ continue;
144
+ }
145
+ if (isComposite(value)) {
146
+ return { ok: false, code: "INVALID_QUERY_VALUE" };
147
+ }
148
+ if (typeof value !== "string" &&
149
+ typeof value !== "number" &&
150
+ typeof value !== "boolean") {
151
+ return { ok: false, code: "INVALID_QUERY_VALUE" };
152
+ }
153
+ if (typeof value === "number" && !Number.isFinite(value)) {
154
+ return { ok: false, code: "INVALID_QUERY_VALUE" };
155
+ }
156
+ local.set(key, serializeScalar(value));
157
+ }
158
+ for (const [key, value] of local) {
159
+ if (value === "delete") {
160
+ map.delete(key);
161
+ }
162
+ else {
163
+ map.set(key, value);
164
+ }
165
+ }
166
+ }
167
+ const keys = [...map.keys()].sort((a, b) => {
168
+ // Unicode code point order (UTF-16 code unit order matches for BMP + surrogate pairs as code points when comparing well-formed strings via localeCompare with 'kn' or by iterating code points).
169
+ const aPoints = [...a];
170
+ const bPoints = [...b];
171
+ const n = Math.min(aPoints.length, bPoints.length);
172
+ for (let i = 0; i < n; i++) {
173
+ const ca = aPoints[i].codePointAt(0);
174
+ const cb = bPoints[i].codePointAt(0);
175
+ if (ca !== cb) {
176
+ return ca - cb;
177
+ }
178
+ }
179
+ return aPoints.length - bPoints.length;
180
+ });
181
+ if (keys.length === 0) {
182
+ return { ok: true, url: parsed.path + parsed.fragment };
183
+ }
184
+ const query = keys
185
+ .map((key) => `${encodeRFC3986(key)}=${encodeRFC3986(map.get(key))}`)
186
+ .join("&");
187
+ return { ok: true, url: `${parsed.path}?${query}${parsed.fragment}` };
188
+ }