History

refactor: consolidate JSON parsing functions into json-form module

Rhys Sullivan committed 7 months ago

Commit af98c571 parent b8a09bd4 files · +120 −158Browse files

Unified · Split

4 files
  1. apps/web/src/views/add-source.tsx +1 −62
  2. apps/web/src/views/json-form.ts +74 −0
  3. apps/web/src/views/source-editor.tsx +1 −62
  4. packages/platform/control-plane/src/runtime/execution/live.ts +44 −34
apps/web/src/views/add-source.tsx+1−62
@@ -51,6 +51,7 @@ import { · 50 unmodified lines
51 type McpTransportFields,51 type McpTransportFields,
52 type McpTransportValue,52 type McpTransportValue,
53} from "./mcp-transport-state";53} from "./mcp-transport-state";
54import { parseJsonStringArray, parseJsonStringMap } from "./json-form";
54import { getDomain } from "tldts";55import { getDomain } from "tldts";
55 56
56// ---------------------------------------------------------------------------57// ---------------------------------------------------------------------------
@@ -214,68 +215,6 @@ const googleDiscoveryDefaultsFromUrl = ( · 157 unmodified lines
214 }215 }
215};216};
216 217
217const parseJsonStringMap = (
218 label: string,
219 text: string,
220): Record<string, string> | null => {
221 const trimmed = text.trim();
222 if (trimmed.length === 0) {
223 return null;
224 }
225
226 let parsed: unknown;
227 try {
228 parsed = JSON.parse(trimmed);
229 } catch {
230 throw new Error(`${label} must be valid JSON.`);
231 }
232
233 if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
234 throw new Error(`${label} must be a JSON object with string values.`);
235 }
236
237 const entries = Object.entries(parsed as Record<string, unknown>);
238 const normalized: Record<string, string> = {};
239 for (const [key, value] of entries) {
240 if (typeof value !== "string") {
241 throw new Error(`${label} must only contain string values.`);
242 }
243 normalized[key] = value;
244 }
245
246 return Object.keys(normalized).length === 0 ? null : normalized;
247};
248
249const parseJsonStringArray = (
250 label: string,
251 text: string,
252): Array<string> | null => {
253 const trimmed = text.trim();
254 if (trimmed.length === 0) {
255 return null;
256 }
257
258 let parsed: unknown;
259 try {
260 parsed = JSON.parse(trimmed);
261 } catch {
262 throw new Error(`${label} must be valid JSON.`);
263 }
264
265 if (
266 !Array.isArray(parsed) ||
267 !parsed.every((value) => typeof value === "string")
268 ) {
269 throw new Error(`${label} must be a JSON array of strings.`);
270 }
271
272 const normalized = parsed
273 .map((value) => value.trim())
274 .filter((value) => value.length > 0);
275
276 return normalized.length > 0 ? normalized : null;
277};
278
279const stringifyStringMap = (218const stringifyStringMap = (
280 value: Readonly<Record<string, string>> | null | undefined,219 value: Readonly<Record<string, string>> | null | undefined,
281): string =>220): string =>
apps/web/src/views/json-form.ts+74−0
@@ -0,0 +1,74 @@
1import * as Either from "effect/Either";
2import * as ParseResult from "effect/ParseResult";
3import * as Schema from "effect/Schema";
4
5const JsonStringMapSchema = Schema.parseJson(
6 Schema.Record({
7 key: Schema.String,
8 value: Schema.String,
9 }),
10);
11
12const JsonStringArraySchema = Schema.parseJson(
13 Schema.Array(Schema.String),
14);
15
16const formatJsonFieldError = (
17 label: string,
18 error: ParseResult.ParseError,
19): Error =>
20 new Error(
21 `${label} is invalid: ${ParseResult.TreeFormatter.formatErrorSync(error)}`,
22 );
23
24const decodeJsonField = <A>(input: {
25 label: string;
26 text: string;
27 schema: Schema.Schema<A, string>;
28}): A | null => {
29 const trimmed = input.text.trim();
30 if (trimmed.length === 0) {
31 return null;
32 }
33
34 const decoded = Schema.decodeUnknownEither(input.schema)(trimmed);
35 if (Either.isLeft(decoded)) {
36 throw formatJsonFieldError(input.label, decoded.left);
37 }
38
39 return decoded.right;
40};
41
42export const parseJsonStringMap = (
43 label: string,
44 text: string,
45): Record<string, string> | null => {
46 const decoded = decodeJsonField({
47 label,
48 text,
49 schema: JsonStringMapSchema,
50 });
51
52 return decoded && Object.keys(decoded).length > 0 ? decoded : null;
53};
54
55export const parseJsonStringArray = (
56 label: string,
57 text: string,
58): Array<string> | null => {
59 const decoded = decodeJsonField({
60 label,
61 text,
62 schema: JsonStringArraySchema,
63 });
64
65 if (!decoded) {
66 return null;
67 }
68
69 const normalized = decoded
70 .map((value) => value.trim())
71 .filter((value) => value.length > 0);
72
73 return normalized.length > 0 ? normalized : null;
74};
apps/web/src/views/source-editor.tsx+1−62
@@ -44,6 +44,7 @@ import { · 43 unmodified lines
44 type McpTransportFields,44 type McpTransportFields,
45 type McpTransportValue,45 type McpTransportValue,
46} from "./mcp-transport-state";46} from "./mcp-transport-state";
47import { parseJsonStringArray, parseJsonStringMap } from "./json-form";
47import { sourceTemplates, type SourceTemplate } from "./source-templates";48import { sourceTemplates, type SourceTemplate } from "./source-templates";
48import { getDomain } from "tldts";49import { getDomain } from "tldts";
49 50
@@ -357,36 +358,6 @@ const readBindingTransport = (source: Source): McpTransportValue => { · 307 unmodified lines
357 : "";358 : "";
358};359};
359 360
360const parseJsonStringArray = (
361 label: string,
362 text: string,
363): Array<string> | null => {
364 const trimmed = text.trim();
365 if (trimmed.length === 0) {
366 return null;
367 }
368
369 let parsed: unknown;
370 try {
371 parsed = JSON.parse(trimmed);
372 } catch {
373 throw new Error(`${label} must be valid JSON.`);
374 }
375
376 if (
377 !Array.isArray(parsed) ||
378 !parsed.every((value) => typeof value === "string")
379 ) {
380 throw new Error(`${label} must be a JSON array of strings.`);
381 }
382
383 const normalized = parsed
384 .map((value) => value.trim())
385 .filter((value) => value.length > 0);
386
387 return normalized.length > 0 ? normalized : null;
388};
389
390const buildSyntheticMcpStdioEndpoint = (input: {361const buildSyntheticMcpStdioEndpoint = (input: {
391 name?: string | null;362 name?: string | null;
392 endpoint?: string | null;363 endpoint?: string | null;
@@ -507,38 +478,6 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 114 unmodified lines
507 : {}),478 : {}),
508});479});
509 480
510const parseJsonStringMap = (
511 label: string,
512 text: string,
513): Record<string, string> | null => {
514 const trimmed = text.trim();
515 if (trimmed.length === 0) {
516 return null;
517 }
518
519 let parsed: unknown;
520 try {
521 parsed = JSON.parse(trimmed);
522 } catch {
523 throw new Error(`${label} must be valid JSON.`);
524 }
525
526 if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
527 throw new Error(`${label} must be a JSON object with string values.`);
528 }
529
530 const entries = Object.entries(parsed as Record<string, unknown>);
531 const normalized: Record<string, string> = {};
532 for (const [key, value] of entries) {
533 if (typeof value !== "string") {
534 throw new Error(`${label} must only contain string values.`);
535 }
536 normalized[key] = value;
537 }
538
539 return Object.keys(normalized).length === 0 ? null : normalized;
540};
541
542const buildAuthPayload = (481const buildAuthPayload = (
543 state: SourceFormState,482 state: SourceFormState,
544): CreateSourcePayload["auth"] => {483): CreateSourcePayload["auth"] => {
packages/platform/control-plane/src/runtime/execution/live.ts+44−34
@@ -193,31 +193,40 @@ export const createLiveExecutionManager = () => { · 192 unmodified lines
193 response,193 response,
194 };194 };
195 195
196 yield* publishState({196 return yield* Effect.gen(function* () {
197 executionId,197 yield* publishState({
198 state: "waiting_for_interaction",198 executionId,
199 });199 state: "waiting_for_interaction",
200 200 });
201 const resolved = yield* Deferred.await(response);201
202 const resolvedAt = Date.now();202 const resolved = yield* Deferred.await(response);
203 203 const resolvedAt = Date.now();
204 yield* rows.executionInteractions.update(interaction.id, {204
205 status: resolved.action === "cancel" ? "cancelled" : "resolved",205 yield* rows.executionInteractions.update(interaction.id, {
206 responseJson: serializeJson(sanitizePersistedElicitationResponse(resolved)),206 status: resolved.action === "cancel" ? "cancelled" : "resolved",
207 responsePrivateJson: serializeJson(resolved),207 responseJson: serializeJson(sanitizePersistedElicitationResponse(resolved)),
208 updatedAt: resolvedAt,208 responsePrivateJson: serializeJson(resolved),
209 });209 updatedAt: resolvedAt,
210 yield* rows.executions.update(executionId, {210 });
211 status: "running",211 yield* rows.executions.update(executionId, {
212 updatedAt: resolvedAt,212 status: "running",
213 });213 updatedAt: resolvedAt,
214 yield* publishState({214 });
215 executionId,215 yield* publishState({
216 state: "running",216 executionId,
217 });217 state: "running",
218 218 });
219 run.currentInteraction = null;219
220 return resolved;220 return resolved;
221 }).pipe(
222 Effect.ensuring(
223 Effect.sync(() => {
224 if (run.currentInteraction?.interactionId === interaction.id) {
225 run.currentInteraction = null;
226 }
227 }),
228 ),
229 );
221 }),230 }),
222 231
223 resolveInteraction: ({ executionId, response }) =>232 resolveInteraction: ({ executionId, response }) =>
@@ -234,20 +243,21 @@ export const createLiveExecutionManager = () => { · 10 unmodified lines
234 243
235 finishRun: ({ executionId, state }) =>244 finishRun: ({ executionId, state }) =>
236 publishState({ executionId, state }).pipe(245 publishState({ executionId, state }).pipe(
237 Effect.zipRight(246 Effect.zipRight(clearMcpConnectionPoolRun(executionId)),
238 clearMcpConnectionPoolRun(executionId).pipe(247 Effect.ensuring(
239 Effect.zipRight(Effect.sync(() => {248 Effect.sync(() => {
240 runs.delete(executionId);249 runs.delete(executionId);
241 })),250 }),
242 ),
243 ),251 ),
244 ),252 ),
245 253
246 clearRun: (executionId) =>254 clearRun: (executionId) =>
247 clearMcpConnectionPoolRun(executionId).pipe(255 clearMcpConnectionPoolRun(executionId).pipe(
248 Effect.zipRight(Effect.sync(() => {256 Effect.ensuring(
249 runs.delete(executionId);257 Effect.sync(() => {
250 })),258 runs.delete(executionId);
259 }),
260 ),
251 ),261 ),
252 } satisfies LiveExecutionManagerShape;262 } satisfies LiveExecutionManagerShape;
253 263