Skip to content

Add documentations and metadata to discovery #498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 54 additions & 9 deletions packages/restate-sdk/src/endpoint/endpoint_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ function isWorkflowDefinition<P extends string, M>(
return m && m.workflow !== undefined;
}

/**
* Services can have additional information that is not part of the definition.
* For example a description or metadata.
*/
type ServiceAuxInfo = {
description?: string;
metadata?: Record<string, any>;
};

export class EndpointBuilder {
private readonly services: Map<string, Component> = new Map();
public loggerTransport: LoggerTransport = defaultLoggerTransport;
Expand Down Expand Up @@ -88,19 +97,31 @@ export class EndpointBuilder {
if (!service) {
throw new TypeError(`no service implementation found.`);
}
this.bindServiceComponent(name, service);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.bindServiceComponent(name, service, definition as ServiceAuxInfo);
} else if (isObjectDefinition(definition)) {
const { name, object } = definition;
if (!object) {
throw new TypeError(`no object implementation found.`);
}
this.bindVirtualObjectComponent(name, object);

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.bindVirtualObjectComponent(
name,
object,
definition as ServiceAuxInfo
);
} else if (isWorkflowDefinition(definition)) {
const { name, workflow } = definition;
if (!workflow) {
throw new TypeError(`no workflow implementation found.`);
}
this.bindWorkflowObjectComponent(name, workflow);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.bindWorkflowObjectComponent(
name,
workflow,
definition as ServiceAuxInfo
);
} else {
throw new TypeError(
"can only bind a service or a virtual object or a workflow definition"
Expand Down Expand Up @@ -133,11 +154,19 @@ export class EndpointBuilder {
return endpoint;
}

private bindServiceComponent(name: string, router: any) {
private bindServiceComponent(
name: string,
router: any,
definition: ServiceAuxInfo
) {
if (name.indexOf("/") !== -1) {
throw new Error("service name must not contain any slash '/'");
}
const component = new ServiceComponent(name);
const component = new ServiceComponent(
name,
definition.description,
definition.metadata
);

for (const [route, handler] of Object.entries(
router as { [s: string]: any }
Expand All @@ -153,11 +182,19 @@ export class EndpointBuilder {
this.addComponent(component);
}

private bindVirtualObjectComponent(name: string, router: any) {
private bindVirtualObjectComponent(
name: string,
router: any,
definition: ServiceAuxInfo
) {
if (name.indexOf("/") !== -1) {
throw new Error("service name must not contain any slash '/'");
}
const component = new VirtualObjectComponent(name);
const component = new VirtualObjectComponent(
name,
definition.description,
definition.metadata
);

for (const [route, handler] of Object.entries(
router as { [s: string]: any }
Expand All @@ -172,11 +209,19 @@ export class EndpointBuilder {
this.addComponent(component);
}

private bindWorkflowObjectComponent(name: string, workflow: any) {
private bindWorkflowObjectComponent(
name: string,
workflow: any,
definition: ServiceAuxInfo
) {
if (name.indexOf("/") !== -1) {
throw new Error("service name must not contain any slash '/'");
}
const component = new WorkflowComponent(name);
const component = new WorkflowComponent(
name,
definition.description,
definition.metadata
);

for (const [route, handler] of Object.entries(
workflow as { [s: string]: any }
Expand Down
44 changes: 35 additions & 9 deletions packages/restate-sdk/src/types/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ export interface ComponentHandler {
export class ServiceComponent implements Component {
private readonly handlers: Map<string, ServiceHandler> = new Map();

constructor(private readonly componentName: string) {}
constructor(
private readonly componentName: string,
public readonly description?: string,
public readonly metadata?: Record<string, string>
) {}

name(): string {
return this.componentName;
Expand All @@ -66,15 +70,19 @@ export class ServiceComponent implements Component {
contentType:
serviceHandler.handlerWrapper.contentType ?? "application/json",
},
};
documentation: serviceHandler.handlerWrapper.description,
metadata: serviceHandler.handlerWrapper.metadata,
} satisfies d.Handler;
}
);

return {
name: this.componentName,
ty: d.ServiceType.SERVICE,
handlers,
};
documentations: this.description,
metadata: this.metadata,
} satisfies d.Service;
}

handlerMatching(url: InvokePathComponents): ComponentHandler | undefined {
Expand Down Expand Up @@ -121,7 +129,11 @@ export class ServiceHandler implements ComponentHandler {
export class VirtualObjectComponent implements Component {
private readonly handlers: Map<string, HandlerWrapper> = new Map();

constructor(public readonly componentName: string) {}
constructor(
public readonly componentName: string,
public readonly description?: string,
public readonly metadata?: Record<string, string>
) {}

name(): string {
return this.componentName;
Expand All @@ -148,15 +160,20 @@ export class VirtualObjectComponent implements Component {
opts.kind === HandlerKind.EXCLUSIVE
? d.ServiceHandlerType.EXCLUSIVE
: d.ServiceHandlerType.SHARED,
};

documentation: opts.description,
metadata: opts.metadata,
} satisfies d.Handler;
}
);

return {
name: this.componentName,
ty: d.ServiceType.VIRTUAL_OBJECT,
handlers,
};
documentations: this.description,
metadata: this.metadata,
} satisfies d.Service;
}

handlerMatching(url: InvokePathComponents): ComponentHandler | undefined {
Expand Down Expand Up @@ -196,7 +213,11 @@ export class VirtualObjectHandler implements ComponentHandler {
export class WorkflowComponent implements Component {
private readonly handlers: Map<string, HandlerWrapper> = new Map();

constructor(public readonly componentName: string) {}
constructor(
public readonly componentName: string,
public readonly description?: string,
public readonly metadata?: Record<string, string>
) {}

name(): string {
return this.componentName;
Expand All @@ -223,15 +244,20 @@ export class WorkflowComponent implements Component {
handler.kind === HandlerKind.WORKFLOW
? d.ServiceHandlerType.WORKFLOW
: d.ServiceHandlerType.SHARED,
};

documentation: handler.description,
metadata: handler.metadata,
} satisfies d.Handler;
}
);

return {
name: this.componentName,
ty: d.ServiceType.WORKFLOW,
handlers,
};
documentations: this.description,
metadata: this.metadata,
} satisfies d.Service;
}

handlerMatching(url: InvokePathComponents): ComponentHandler | undefined {
Expand Down
4 changes: 4 additions & 0 deletions packages/restate-sdk/src/types/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,16 @@ export interface Handler {
input?: InputPayload;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
output?: OutputPayload;
metadata?: Record<string, string>;
documentation?: string;
}

export interface Service {
name: string;
ty: ServiceType;
handlers: Handler[];
metadata?: Record<string, string>;
documentations?: string;
}

export interface Endpoint {
Expand Down
56 changes: 54 additions & 2 deletions packages/restate-sdk/src/types/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,16 @@ export type ServiceHandlerOpts = {
* in that case, the output parameter is a Uint8Array.
*/
output?: Serde<unknown>;

/**
* An additional description for the handler, for documentation purposes.
*/
description?: string;

/**
* Additional metadata for the handler.
*/
metadata?: Record<string, string>;
};

export type ObjectHandlerOpts = {
Expand Down Expand Up @@ -305,6 +315,16 @@ export type ObjectHandlerOpts = {
* in that case, the output parameter is a Uint8Array.
*/
output?: Serde<unknown>;

/**
* An additional description for the handler, for documentation purposes.
*/
description?: string;

/**
* Additional metadata for the handler.
*/
metadata?: Record<string, string>;
};

export type WorkflowHandlerOpts = {
Expand Down Expand Up @@ -336,6 +356,16 @@ export type WorkflowHandlerOpts = {
* in that case, the output parameter is a Uint8Array.
*/
output?: Serde<unknown>;

/**
* An additional description for the handler, for documentation purposes.
*/
description?: string;

/**
* Additional metadata for the handler.
*/
metadata?: Record<string, string>;
};

const HANDLER_SYMBOL = Symbol("Handler");
Expand All @@ -361,7 +391,9 @@ export class HandlerWrapper {
handlerCopy,
inputSerde,
outputSerde,
opts?.accept
opts?.accept,
opts?.description,
opts?.metadata
);
}

Expand All @@ -378,7 +410,9 @@ export class HandlerWrapper {
private handler: Function,
public readonly inputSerde: Serde<unknown>,
public readonly outputSerde: Serde<unknown>,
accept?: string
accept?: string,
public readonly description?: string,
public readonly metadata?: Record<string, string>
) {
this.accept = accept ? accept : inputSerde.contentType;
this.contentType = outputSerde.contentType;
Expand Down Expand Up @@ -675,12 +709,16 @@ export type ServiceOpts<U> = {
*
* @param name the service name
* @param handlers the handlers for the service
* @param description an optional description for the service
* @param metadata an optional metadata for the service
* @type P the name of the service
* @type M the handlers for the service
*/
export const service = <P extends string, M>(service: {
name: P;
handlers: ServiceOpts<M>;
description?: string;
metadata?: Record<string, string>;
}): ServiceDefinition<P, M> => {
if (!service.handlers) {
throw new Error("service must be defined");
Expand All @@ -701,6 +739,8 @@ export const service = <P extends string, M>(service: {
return {
name: service.name,
service: Object.fromEntries(handlers) as object,
metadata: service.metadata,
description: service.description,
} as ServiceDefinition<P, M>;
};

Expand Down Expand Up @@ -763,10 +803,16 @@ export type ObjectOpts<U> = {
*
* @param name the name of the object
* @param handlers the handlers for the object
* @param description an optional description for the object
* @param metadata an optional metadata for the object
* @type P the name of the object
* @type M the handlers for the object
*/
export const object = <P extends string, M>(object: {
name: P;
handlers: ObjectOpts<M>;
description?: string;
metadata?: Record<string, string>;
}): VirtualObjectDefinition<P, M> => {
if (!object.handlers) {
throw new Error("object options must be defined");
Expand All @@ -789,6 +835,8 @@ export const object = <P extends string, M>(object: {
return {
name: object.name,
object: Object.fromEntries(handlers) as object,
metadata: object.metadata,
description: object.description,
} as VirtualObjectDefinition<P, M>;
};

Expand Down Expand Up @@ -861,6 +909,8 @@ export type WorkflowOpts<U> = {
export const workflow = <P extends string, M>(workflow: {
name: P;
handlers: WorkflowOpts<M>;
description?: string;
metadata?: Record<string, string>;
}): WorkflowDefinition<P, M> => {
if (!workflow.handlers) {
throw new Error("workflow must contain handlers");
Expand Down Expand Up @@ -919,5 +969,7 @@ export const workflow = <P extends string, M>(workflow: {
return {
name: workflow.name,
workflow: Object.fromEntries(handlers) as object,
metadata: workflow.metadata,
description: workflow.description,
} as WorkflowDefinition<P, M>;
};