From 912af3dd5fc7725697b6d916b5f8a924ac39e5f4 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 22 Oct 2026 21:30:05 -0700 Subject: [PATCH 1/2] Added a user-service resource method that returns a WIT record. --- .../samples/dotnet/wit/user-service/Models.cs | 28 ++++++++++++++ .../dotnet/wit/user-service/Resources.cs | 3 ++ .../dotnet/wit/user-service/Services.cs | 33 ++++++++++++++++ .../samples/go/userservice/userservice.go | 38 +++++++++++++++++++ advanced/samples/inputs/user-service.wit | 10 +++++ .../samples/python/tests/test_user_service.py | 9 +++++ .../python/wit/user_service/__init__.py | 6 +++ .../wit/user_service/_resources/user.py | 6 +++ .../samples/python/wit/user_service/models.py | 10 +++++ .../python/wit/user_service/services.py | 21 ++++++++++ .../typescript/tests/user-service.test.ts | 3 ++ .../typescript/wit/user-service/index.ts | 8 +++- .../typescript/wit/user-service/models.ts | 8 ++++ .../typescript/wit/user-service/resources.ts | 5 +++ .../typescript/wit/user-service/services.ts | 18 ++++++++- 15 files changed, 204 insertions(+), 2 deletions(-) diff --git a/advanced/samples/dotnet/wit/user-service/Models.cs b/advanced/samples/dotnet/wit/user-service/Models.cs index bee02cbf5..2cd81a702 100644 --- a/advanced/samples/dotnet/wit/user-service/Models.cs +++ b/advanced/samples/dotnet/wit/user-service/Models.cs @@ -9,6 +9,20 @@ namespace Nexgen.UserService { + [GeneratedCode("nexgen", null)] + public record GetProfileRequest + { + /// + /// Initializes a new instance of the class. + /// + public GetProfileRequest(string userId) + { + UserId = userId; + } + + public string UserId { get; init; } + } + [GeneratedCode("nexgen", null)] public record GetUserRequest { @@ -58,4 +72,18 @@ public UpdateEmailRequest(string userId, string email) public string Email { get; init; } } + [GeneratedCode("nexgen", null)] + public record UserProfile + { + /// + /// Initializes a new instance of the class. + /// + public UserProfile(string displayName) + { + DisplayName = displayName; + } + + public string DisplayName { get; init; } + } + } diff --git a/advanced/samples/dotnet/wit/user-service/Resources.cs b/advanced/samples/dotnet/wit/user-service/Resources.cs index ecd904a4b..a8fbce60d 100644 --- a/advanced/samples/dotnet/wit/user-service/Resources.cs +++ b/advanced/samples/dotnet/wit/user-service/Resources.cs @@ -52,6 +52,9 @@ public async Task NotifyAsync(string? reason, IReadOnlyList channels, st await RequireNexgenClient().ExecuteNexusOperationAsync(svc => svc.Notify(request)).ConfigureAwait(true); } + [GeneratedCode("nexgen", null)] + public Task GetProfileAsync() => throw new NotSupportedException("Resource method is not bound to a Nexus operation."); + } } diff --git a/advanced/samples/dotnet/wit/user-service/Services.cs b/advanced/samples/dotnet/wit/user-service/Services.cs index 231cffe25..6ad0537a0 100644 --- a/advanced/samples/dotnet/wit/user-service/Services.cs +++ b/advanced/samples/dotnet/wit/user-service/Services.cs @@ -29,6 +29,10 @@ internal interface IUserService [NexusOperation("Notify")] void Notify(NotifyRequest request); + [GeneratedCode("nexgen", null)] + [NexusOperation("GetProfile")] + UserProfile GetProfile(GetProfileRequest request); + } [GeneratedCode("nexgen", null)] @@ -80,6 +84,20 @@ public NotifyOptions(string userId, IReadOnlyList channels, string sende public string Sender { get; set; } } + [GeneratedCode("nexgen", null)] + public class GetProfileOptions + { + /// + /// Initializes a new instance of the class. + /// + public GetProfileOptions(string userId) + { + UserId = userId; + } + + public string UserId { get; set; } + } + [GeneratedCode("nexgen", null)] public class UserServiceClient { @@ -137,6 +155,21 @@ public Task NotifyAsync(NotifyOptions options) return NotifyAsync(request); } + [GeneratedCode("nexgen", null)] + private async Task GetProfileAsync(GetProfileRequest request) + { + var result = await _client.ExecuteNexusOperationAsync(svc => svc.GetProfile(request)).ConfigureAwait(true); + return result; + } + + /// Options for the operation. + [GeneratedCode("nexgen", null)] + public Task GetProfileAsync(GetProfileOptions options) + { + var request = new GetProfileRequest(options.UserId); + return GetProfileAsync(request); + } + } } diff --git a/advanced/samples/go/userservice/userservice.go b/advanced/samples/go/userservice/userservice.go index 16d297683..da8796321 100644 --- a/advanced/samples/go/userservice/userservice.go +++ b/advanced/samples/go/userservice/userservice.go @@ -52,6 +52,14 @@ func (m notifyRequest) GetSender() string { return m.Sender } +type getProfileRequest struct { + UserID string +} + +func (m getProfileRequest) GetUserID() string { + return m.UserID +} + // --- Resources --- type User struct { @@ -80,6 +88,10 @@ func (u *User) Notify(ctx workflow.Context, reason string, channels []string, se return notify(ctx, notifyRequest{UserID: u.UserID, Reason: reasonPtr, Channels: channels, Sender: sender}) } +func (u *User) GetProfile(ctx workflow.Context) workflow.Future { + panic("User.GetProfile is not yet implemented") +} + // --- Operations (internal) --- func getUser(ctx workflow.Context, request getUserRequest) workflow.Future { @@ -100,8 +112,23 @@ func notify(ctx workflow.Context, request notifyRequest) workflow.Future { return fut } +func getProfile(ctx workflow.Context, request getProfileRequest) workflow.Future { + c := workflow.NewNexusClient("user-service", "UserService") + fut := c.ExecuteOperation(ctx, "GetProfile", request, workflow.NexusOperationOptions{}) + return fut +} + // --- Operations (public API) --- +type UserProfile struct { + // DisplayName - Required. + DisplayName string +} + +func (m UserProfile) GetDisplayName() string { + return m.DisplayName +} + type GetUserOptions struct { // UserID - Required. UserID string @@ -150,3 +177,14 @@ func Notify(ctx workflow.Context, opts NotifyOptions) workflow.Future { Sender: opts.Sender, }) } + +type GetProfileOptions struct { + // UserID - Required. + UserID string +} + +func GetProfile(ctx workflow.Context, opts GetProfileOptions) workflow.Future { + return getProfile(ctx, getProfileRequest{ + UserID: opts.UserID, + }) +} diff --git a/advanced/samples/inputs/user-service.wit b/advanced/samples/inputs/user-service.wit index d3ee118b1..9c4da7d00 100644 --- a/advanced/samples/inputs/user-service.wit +++ b/advanced/samples/inputs/user-service.wit @@ -10,6 +10,7 @@ interface user-service { update-email: func(email: string) -> user-result; notify: func(reason: option, channels: list, sender: string); + get-profile: func() -> user-profile; } type user-result = own; @@ -30,7 +31,16 @@ interface user-service { sender: string, } + record get-profile-request { + user-id: string, + } + + record user-profile { + display-name: string, + } + get-user: func(request: get-user-request) -> user-result; update-email: func(request: update-email-request) -> user-result; notify: func(request: notify-request); + get-profile: func(request: get-profile-request) -> user-profile; } diff --git a/advanced/samples/python/tests/test_user_service.py b/advanced/samples/python/tests/test_user_service.py index e4f11e0aa..9320bfb45 100644 --- a/advanced/samples/python/tests/test_user_service.py +++ b/advanced/samples/python/tests/test_user_service.py @@ -71,6 +71,15 @@ async def notify( ) -> None: self.calls.append(("Notify", input)) + @sync_operation + async def get_profile( + self, + _ctx: StartOperationContext, + input: user_service_models.GetProfileRequest, + ) -> user_service_models.UserProfile: + self.calls.append(("GetProfile", input)) + return user_service_models.UserProfile(display_name="Alice") + @workflow.defn class UserServiceCallerWorkflow: diff --git a/advanced/samples/python/wit/user_service/__init__.py b/advanced/samples/python/wit/user_service/__init__.py index e3b7b5332..52316ad55 100644 --- a/advanced/samples/python/wit/user_service/__init__.py +++ b/advanced/samples/python/wit/user_service/__init__.py @@ -58,4 +58,10 @@ def __init__( ): _NexusOperationInfo( operation=_services.UserService.notify, ), + ( + "UserService", + "GetProfile", + ): _NexusOperationInfo( + operation=_services.UserService.get_profile, + ), } diff --git a/advanced/samples/python/wit/user_service/_resources/user.py b/advanced/samples/python/wit/user_service/_resources/user.py index 9fc4c2a69..6ae35e039 100644 --- a/advanced/samples/python/wit/user_service/_resources/user.py +++ b/advanced/samples/python/wit/user_service/_resources/user.py @@ -8,6 +8,7 @@ from ..models import ( NotifyRequest, UpdateEmailRequest, + UserProfile, ) @@ -56,6 +57,11 @@ async def notify( ) await handle + async def get_profile( + self, + ) -> UserProfile: + raise NotImplementedError("user.get_profile is not yet implemented") + def bind_user_client( resource: User, diff --git a/advanced/samples/python/wit/user_service/models.py b/advanced/samples/python/wit/user_service/models.py index 1e24e4072..dd2003c56 100644 --- a/advanced/samples/python/wit/user_service/models.py +++ b/advanced/samples/python/wit/user_service/models.py @@ -22,3 +22,13 @@ class NotifyRequest: reason: str | None = None channels: list[str] | None = dataclasses.field(default_factory=list) sender: str + + +@dataclasses.dataclass(slots=True) +class GetProfileRequest: + user_id: str + + +@dataclasses.dataclass(slots=True) +class UserProfile: + display_name: str diff --git a/advanced/samples/python/wit/user_service/services.py b/advanced/samples/python/wit/user_service/services.py index 9d3706b55..5427576a4 100644 --- a/advanced/samples/python/wit/user_service/services.py +++ b/advanced/samples/python/wit/user_service/services.py @@ -7,9 +7,11 @@ import temporalio.workflow from .models import ( + GetProfileRequest, GetUserRequest, NotifyRequest, UpdateEmailRequest, + UserProfile, ) from ._resources.user import ( User, @@ -37,6 +39,11 @@ class UserService: None, ] = Operation(name="Notify") + get_profile: Operation[ + GetProfileRequest, + UserProfile, + ] = Operation(name="GetProfile") + class UserServiceClient: def __init__(self, endpoint: str) -> None: @@ -97,3 +104,17 @@ async def notify( operation="Notify", input=request, ) + + async def get_profile( + self, + *, + user_id: str, + ) -> temporalio.workflow.NexusOperationHandle[UserProfile]: + request = GetProfileRequest( + user_id=user_id, + ) + return await self._nexus_client.start_operation( + operation="GetProfile", + input=request, + output_type=UserProfile, + ) diff --git a/advanced/samples/typescript/tests/user-service.test.ts b/advanced/samples/typescript/tests/user-service.test.ts index 7e7068c60..72af389a6 100644 --- a/advanced/samples/typescript/tests/user-service.test.ts +++ b/advanced/samples/typescript/tests/user-service.test.ts @@ -30,6 +30,9 @@ describe("user-service generated output", () => { return new User(input.userId, input.email); }, async notify() {}, + async getProfile() { + return { displayName: "Alice" }; + }, }); const result = await executeWorkflowWithNexus<{ diff --git a/advanced/samples/typescript/wit/user-service/index.ts b/advanced/samples/typescript/wit/user-service/index.ts index 2027ac46d..7fca3cadc 100644 --- a/advanced/samples/typescript/wit/user-service/index.ts +++ b/advanced/samples/typescript/wit/user-service/index.ts @@ -1,4 +1,10 @@ // Generated by nexgen latest. DO NOT EDIT! export { userService, UserServiceClient } from "./services"; -export type { GetUserRequest, NotifyRequest, UpdateEmailRequest } from "./models"; +export type { + GetProfileRequest, + GetUserRequest, + NotifyRequest, + UpdateEmailRequest, + UserProfile, +} from "./models"; diff --git a/advanced/samples/typescript/wit/user-service/models.ts b/advanced/samples/typescript/wit/user-service/models.ts index d3467197f..9d6ebab71 100644 --- a/advanced/samples/typescript/wit/user-service/models.ts +++ b/advanced/samples/typescript/wit/user-service/models.ts @@ -15,3 +15,11 @@ export interface NotifyRequest { channels?: string[]; sender: string; } + +export interface UserProfile { + displayName: string; +} + +export interface GetProfileRequest { + userId: string; +} diff --git a/advanced/samples/typescript/wit/user-service/resources.ts b/advanced/samples/typescript/wit/user-service/resources.ts index 5053e0855..9e7cf70ef 100644 --- a/advanced/samples/typescript/wit/user-service/resources.ts +++ b/advanced/samples/typescript/wit/user-service/resources.ts @@ -1,6 +1,7 @@ // Generated by nexgen latest. DO NOT EDIT! import * as workflow from "@temporalio/workflow"; +import type { UserProfile } from "./models"; import { userService } from "./services"; const userClient = Symbol("nexgen.User.client"); @@ -40,6 +41,10 @@ export class User { ); await handle.result(); } + + public async getProfile(): Promise { + throw new Error("user.getProfile is not yet implemented"); + } } export function bindUserClient( diff --git a/advanced/samples/typescript/wit/user-service/services.ts b/advanced/samples/typescript/wit/user-service/services.ts index 568534224..344ac25f1 100644 --- a/advanced/samples/typescript/wit/user-service/services.ts +++ b/advanced/samples/typescript/wit/user-service/services.ts @@ -2,7 +2,13 @@ import * as workflow from "@temporalio/workflow"; import * as nexus from "nexus-rpc"; -import type { GetUserRequest, UpdateEmailRequest, NotifyRequest } from "./models"; +import type { + GetUserRequest, + UpdateEmailRequest, + NotifyRequest, + UserProfile, + GetProfileRequest, +} from "./models"; import type { User } from "./resources"; import { bindUserClient } from "./resources"; @@ -10,6 +16,7 @@ export const userService = nexus.service("UserService", { getUser: nexus.operation({ name: "GetUser" }), updateEmail: nexus.operation({ name: "UpdateEmail" }), notify: nexus.operation({ name: "Notify" }), + getProfile: nexus.operation({ name: "GetProfile" }), }); export class UserServiceClient { @@ -54,4 +61,13 @@ export class UserServiceClient { ): Promise> { return await this.client.startOperation(userService.operations.notify, request); } + + /** + * @param request - Request for the operation. + */ + public async getProfile( + request: GetProfileRequest, + ): Promise> { + return await this.client.startOperation(userService.operations.getProfile, request); + } } From e3a5c387f5e7b9dbeff13d661c28abc86dd97759 Mon Sep 17 00:00:00 2001 From: Mohammad Dashti Date: Thu, 22 Oct 2026 21:32:35 -0700 Subject: [PATCH 2/2] Bound resource methods that return a WIT record to their operation. --- .../samples/dotnet/wit/user-service/Resources.cs | 7 ++++++- advanced/samples/go/userservice/userservice.go | 2 +- advanced/samples/python/tests/test_user_service.py | 10 ++++++++-- .../python/wit/user_service/_resources/user.py | 12 +++++++++++- .../samples/typescript/wit/user-service/resources.ts | 7 ++++++- changelog/fixed/bound-bobcat.md | 1 + src/planning/resource_resolution.rs | 3 +++ 7 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 changelog/fixed/bound-bobcat.md diff --git a/advanced/samples/dotnet/wit/user-service/Resources.cs b/advanced/samples/dotnet/wit/user-service/Resources.cs index a8fbce60d..562d4a77f 100644 --- a/advanced/samples/dotnet/wit/user-service/Resources.cs +++ b/advanced/samples/dotnet/wit/user-service/Resources.cs @@ -53,7 +53,12 @@ public async Task NotifyAsync(string? reason, IReadOnlyList channels, st } [GeneratedCode("nexgen", null)] - public Task GetProfileAsync() => throw new NotSupportedException("Resource method is not bound to a Nexus operation."); + public async Task GetProfileAsync() + { + var request = new GetProfileRequest(UserId); + var result = await RequireNexgenClient().ExecuteNexusOperationAsync(svc => svc.GetProfile(request)).ConfigureAwait(true); + return result; + } } diff --git a/advanced/samples/go/userservice/userservice.go b/advanced/samples/go/userservice/userservice.go index da8796321..dfa3e0c63 100644 --- a/advanced/samples/go/userservice/userservice.go +++ b/advanced/samples/go/userservice/userservice.go @@ -89,7 +89,7 @@ func (u *User) Notify(ctx workflow.Context, reason string, channels []string, se } func (u *User) GetProfile(ctx workflow.Context) workflow.Future { - panic("User.GetProfile is not yet implemented") + return getProfile(ctx, getProfileRequest{UserID: u.UserID}) } // --- Operations (internal) --- diff --git a/advanced/samples/python/tests/test_user_service.py b/advanced/samples/python/tests/test_user_service.py index 9320bfb45..1b57bc04f 100644 --- a/advanced/samples/python/tests/test_user_service.py +++ b/advanced/samples/python/tests/test_user_service.py @@ -88,6 +88,7 @@ async def run(self) -> User: service = user_service.UserServiceClient("user-service") user = await service.get_user(user_id="user-123") await user.notify(sender="ops") + assert (await user.get_profile()).display_name == "Alice" return await user.update_email("new@example.com") @@ -135,7 +136,7 @@ async def test_get_user_returns_user_resource(env: WorkflowEnvironment) -> None: assert user.user_id == "user-123" assert user.email == "new@example.com" - assert len(service_handler.calls) == 3 + assert len(service_handler.calls) == 4 get_user_operation, get_user_request = service_handler.calls[0] assert get_user_operation == "GetUser" assert isinstance(get_user_request, user_service_models.GetUserRequest) @@ -147,7 +148,12 @@ async def test_get_user_returns_user_resource(env: WorkflowEnvironment) -> None: user_id="user-123", reason=None, channels=[], sender="ops" ) - update_operation, update_request = service_handler.calls[2] + assert service_handler.calls[2] == ( + "GetProfile", + user_service_models.GetProfileRequest(user_id="user-123"), + ) + + update_operation, update_request = service_handler.calls[3] assert update_operation == "UpdateEmail" assert isinstance(update_request, user_service_models.UpdateEmailRequest) assert update_request.user_id == "user-123" diff --git a/advanced/samples/python/wit/user_service/_resources/user.py b/advanced/samples/python/wit/user_service/_resources/user.py index 6ae35e039..4ca57a541 100644 --- a/advanced/samples/python/wit/user_service/_resources/user.py +++ b/advanced/samples/python/wit/user_service/_resources/user.py @@ -6,6 +6,7 @@ import typing from ..models import ( + GetProfileRequest, NotifyRequest, UpdateEmailRequest, UserProfile, @@ -60,7 +61,16 @@ async def notify( async def get_profile( self, ) -> UserProfile: - raise NotImplementedError("user.get_profile is not yet implemented") + request = GetProfileRequest( + user_id=self.user_id, + ) + nexus_client = getattr(self, "_nexus_client") + handle = await nexus_client.start_operation( + operation="GetProfile", + input=request, + output_type=UserProfile, + ) + return await handle def bind_user_client( diff --git a/advanced/samples/typescript/wit/user-service/resources.ts b/advanced/samples/typescript/wit/user-service/resources.ts index 9e7cf70ef..94df9060d 100644 --- a/advanced/samples/typescript/wit/user-service/resources.ts +++ b/advanced/samples/typescript/wit/user-service/resources.ts @@ -43,7 +43,12 @@ export class User { } public async getProfile(): Promise { - throw new Error("user.getProfile is not yet implemented"); + const request = { userId: this.userId }; + const handle = await requireUserClient(this).startOperation( + userService.operations.getProfile, + request, + ); + return await handle.result(); } } diff --git a/changelog/fixed/bound-bobcat.md b/changelog/fixed/bound-bobcat.md new file mode 100644 index 000000000..0385d3f04 --- /dev/null +++ b/changelog/fixed/bound-bobcat.md @@ -0,0 +1 @@ +A resource method binds to an operation that returns a WIT record, in place of a stub. diff --git a/src/planning/resource_resolution.rs b/src/planning/resource_resolution.rs index 57cda5d4e..91c949ca7 100644 --- a/src/planning/resource_resolution.rs +++ b/src/planning/resource_resolution.rs @@ -429,6 +429,9 @@ fn resource_method_result_matches_operation( operation_output_ref(operation) == Some(proto_name.as_str()) || operation_output_resource_message_name(operation) == Some(proto_name.as_str()) } + TypeSpec::Record(record_name) => { + operation_output_ref(operation) == Some(record_name.as_str()) + } _ => false, } }