Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7863ce3e12 | |||
| ea51ca89e7 | |||
| c3fe7c86b8 | |||
| 74f00fe28f |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
"sourceRepo": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos",
|
||||||
"sourceCommit": "0355d845d7ad4b797343b681e9b5e1a77c63c720",
|
"sourceCommit": "db6d7990a4ffec273258ac313b7d2aeac57b261a",
|
||||||
"sourcePath": "quixos-protocol",
|
"sourcePath": "quixos-protocol",
|
||||||
"exportName": "quixos-protocol",
|
"exportName": "quixos-protocol",
|
||||||
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
|
"mirrorRemote": "https://gitea-external.egads.tutti.syntaxblitz.net/quixos/quixos-protocol.git"
|
||||||
|
|||||||
+28
-12
@@ -285,19 +285,35 @@ export const generateTypeScriptBindings = (
|
|||||||
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
|
genericImplementationType(definition, [...schema.interfaces, ...(schema.interfaceTemplates ?? [])], type),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
const contracts = schema.interfaces.map((iface) => {
|
// Unused imported interfaces must not introduce new message-codec obligations.
|
||||||
const generated = port({
|
// A used port still fails above if its own required codec is missing.
|
||||||
id: capabilityId.dependencyPort("contract"),
|
const hasCodec = (value: ValueType): boolean => {
|
||||||
displayName: iface.displayName,
|
if (value.kind === "message") return !!options.messages?.[value.descriptorId];
|
||||||
requirement: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
if (value.kind === "record") return Object.values(value.fields).every(hasCodec);
|
||||||
|
if (value.kind === "list" || value.kind === "optional") return hasCodec(value.value);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const contracts = schema.interfaces
|
||||||
|
.filter((iface) =>
|
||||||
|
iface.members.every((member) =>
|
||||||
|
member.operations
|
||||||
|
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||||
|
.every((operation) => hasCodec(operation.inputType) && hasCodec(operation.outputType)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((iface) => {
|
||||||
|
const generated = port({
|
||||||
|
id: capabilityId.dependencyPort("contract"),
|
||||||
|
displayName: iface.displayName,
|
||||||
|
requirement: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
||||||
|
});
|
||||||
|
const spec = generated.spec as { operations: unknown };
|
||||||
|
return {
|
||||||
|
iface,
|
||||||
|
type: generated.type,
|
||||||
|
code: `defineQxInterfaceContract<${generated.type}>(${q(iface.revisionId)}, ${JSON.stringify(spec.operations)})`,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
const spec = generated.spec as { operations: unknown };
|
|
||||||
return {
|
|
||||||
iface,
|
|
||||||
type: generated.type,
|
|
||||||
code: `defineQxInterfaceContract<${generated.type}>(${q(iface.revisionId)}, ${JSON.stringify(spec.operations)})`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
const imports = [...messages].map(([id, alias]) => {
|
const imports = [...messages].map(([id, alias]) => {
|
||||||
const binding = options.messages![id]!;
|
const binding = options.messages![id]!;
|
||||||
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export))
|
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(binding.export))
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ declare module "@quixos/web-studio-react-runtime" {
|
|||||||
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}};
|
export type WritableField<T> = ReadableField<T> & {readonly $writeType?: (value: T) => T; readonly writable: true; readonly capability: FieldCapability & {setOperationId: string}};
|
||||||
export type LiveFieldProp<T> = ReadableField<T>;
|
export type LiveFieldProp<T> = ReadableField<T>;
|
||||||
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
|
export type InterfaceReference<I extends string, Fields> = {readonly $quixosRef: string; readonly interfaceRevisionId: I; readonly fields: Fields};
|
||||||
const conformanceView: unique symbol;
|
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly $viewType?: (view: View) => View};
|
||||||
export type ReactInterfaceContract<View> = {readonly interfaceRevisionId: string; readonly [conformanceView]: (view: View) => View};
|
|
||||||
export function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<View>;
|
export function defineReactInterfaceContract<View>(interfaceRevisionId: string, interfaces: unknown): ReactInterfaceContract<View>;
|
||||||
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
export function tryConform<View>(object: string | {readonly $quixosRef: string}, contract: ReactInterfaceContract<View>, options?: {signal?: AbortSignal}): Promise<View | undefined>;
|
||||||
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
export type ConformanceResult<View> = {status: "loading"} | {status: "absent"} | {status: "available"; view: View} | {status: "error"; error: Error};
|
||||||
|
|||||||
+36
-13
@@ -78,21 +78,44 @@ export function generateReactBindings(
|
|||||||
throw new Error("React component check must name a local module relative to generated bindings");
|
throw new Error("React component check must name a local module relative to generated bindings");
|
||||||
return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
|
return `type Component${i} = CheckedComponent<${q(component.propsExport)}, typeof import(${q(component.module)})["default"]>;`;
|
||||||
});
|
});
|
||||||
const contracts = schema.interfaces.map((iface) => {
|
// Emit optional discovery descriptors only when the browser has all codecs.
|
||||||
const reference = type({
|
// Unrelated opaque-message imports must not break an otherwise checked UI.
|
||||||
kind: "object-ref",
|
const browserValue = (value: ValueType, seen: Set<string>): boolean => {
|
||||||
expectation: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
if (value.kind === "message") return false;
|
||||||
});
|
if (value.kind === "builtin") return value.name === "unit";
|
||||||
const calls = iface.members.flatMap((member) =>
|
if (value.kind === "record") return Object.values(value.fields).every((field) => browserValue(field, seen));
|
||||||
|
if (value.kind === "list" || value.kind === "optional") return browserValue(value.value, seen);
|
||||||
|
if (value.kind === "object-ref" && value.expectation.kind === "interface") {
|
||||||
|
const id = value.expectation.interfaceRevisionId;
|
||||||
|
if (seen.has(id)) return true;
|
||||||
|
const contract = schema.interfaces.find((entry) => entry.revisionId === id);
|
||||||
|
return !!contract && browserInterface(contract, new Set([...seen, id]));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const browserInterface = (iface: (typeof schema.interfaces)[number], seen: Set<string>): boolean =>
|
||||||
|
iface.members.every((member) =>
|
||||||
member.operations
|
member.operations
|
||||||
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
.filter((op) => op.mode === "call" && op.scope !== "class")
|
||||||
.map(
|
.every((op) => browserValue(op.inputType, seen) && browserValue(op.outputType, seen)),
|
||||||
(operation) =>
|
|
||||||
`${q(`${member.displayName}.${operation.displayName}`)}: (${operation.inputType.kind === "builtin" && operation.inputType.name === "unit" ? "" : `input: ${type(operation.inputType)}`}) => Promise<${type(operation.outputType)}>`,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
|
const contracts = schema.interfaces
|
||||||
});
|
.filter((iface) => browserInterface(iface, new Set([iface.revisionId])))
|
||||||
|
.map((iface) => {
|
||||||
|
const reference = type({
|
||||||
|
kind: "object-ref",
|
||||||
|
expectation: { kind: "interface", interfaceRevisionId: iface.revisionId },
|
||||||
|
});
|
||||||
|
const calls = iface.members.flatMap((member) =>
|
||||||
|
member.operations
|
||||||
|
.filter((operation) => operation.mode === "call" && operation.scope !== "class")
|
||||||
|
.map(
|
||||||
|
(operation) =>
|
||||||
|
`${q(`${member.displayName}.${operation.displayName}`)}: (${operation.inputType.kind === "builtin" && operation.inputType.name === "unit" ? "" : `input: ${type(operation.inputType)}`}) => Promise<${type(operation.outputType)}>`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return { iface, view: `${reference} & {call: {${calls.join(";")}}}` };
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
`// Generated from checked QX contracts. Do not edit.\nimport {defineReactInterfaceContract} from "@quixos/web-studio-react-runtime";\nimport type {ObjectRef, InterfaceReference, ReadableField, WritableField} from "@quixos/web-studio-react-runtime";\n${declarations.join("\n")}\nexport type ReactResults = {${results.join(";\n")}};\n` +
|
||||||
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
|
`const contractsData = ${JSON.stringify(schema.interfaces)};\n` +
|
||||||
|
|||||||
@@ -33,6 +33,18 @@ test("React bindings preserve read-only, writable and nested reference contracts
|
|||||||
interfaces: [iface.resource.revision],
|
interfaces: [iface.resource.revision],
|
||||||
packages: [pkg.resource.revision],
|
packages: [pkg.resource.revision],
|
||||||
} as const;
|
} as const;
|
||||||
|
const opaque = structuredClone(iface.resource.revision);
|
||||||
|
opaque.revisionId = "opaque@1" as typeof opaque.revisionId;
|
||||||
|
opaque.displayName = "Opaque";
|
||||||
|
for (const member of opaque.members)
|
||||||
|
for (const operation of member.operations)
|
||||||
|
operation.outputType = { kind: "message", descriptorId: "UnsupportedBrowserMessage" };
|
||||||
|
const withUnusedOpaque = generateReactBindings(
|
||||||
|
{ ...schema, interfaces: [...schema.interfaces, opaque], packages: [...schema.packages] },
|
||||||
|
"p@1",
|
||||||
|
["props"],
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(withUnusedOpaque, /"opaque@1": defineReactInterfaceContract/);
|
||||||
const generated = generateReactBindings(
|
const generated = generateReactBindings(
|
||||||
{ ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] },
|
{ ...schema, interfaces: [...schema.interfaces], packages: [...schema.packages] },
|
||||||
"p@1",
|
"p@1",
|
||||||
|
|||||||
Reference in New Issue
Block a user