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..562d4a77f 100644 --- a/advanced/samples/dotnet/wit/user-service/Resources.cs +++ b/advanced/samples/dotnet/wit/user-service/Resources.cs @@ -52,6 +52,14 @@ public async Task NotifyAsync(string? reason, IReadOnlyList channels, st await RequireNexgenClient().ExecuteNexusOperationAsync(svc => svc.Notify(request)).ConfigureAwait(true); } + [GeneratedCode("nexgen", null)] + 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/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..dfa3e0c63 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 { + return getProfile(ctx, getProfileRequest{UserID: u.UserID}) +} + // --- 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..1b57bc04f 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: @@ -79,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") @@ -126,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) @@ -138,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/__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..4ca57a541 100644 --- a/advanced/samples/python/wit/user_service/_resources/user.py +++ b/advanced/samples/python/wit/user_service/_resources/user.py @@ -6,8 +6,10 @@ import typing from ..models import ( + GetProfileRequest, NotifyRequest, UpdateEmailRequest, + UserProfile, ) @@ -56,6 +58,20 @@ async def notify( ) await handle + async def get_profile( + self, + ) -> UserProfile: + 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( 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..94df9060d 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,15 @@ export class User { ); await handle.result(); } + + public async getProfile(): Promise { + const request = { userId: this.userId }; + const handle = await requireUserClient(this).startOperation( + userService.operations.getProfile, + request, + ); + return await handle.result(); + } } 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); + } } 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, } }