From b4b7b91cf7ea5a2665bf4a54728848b098087de0 Mon Sep 17 00:00:00 2001 From: Deluan Date: Mon, 29 Dec 2025 23:38:08 -0500 Subject: [PATCH] feat(plugins): implementation of the Navidrome Plugin Development Kit with generated client wrappers and service interfaces - Phase 3 Signed-off-by: Deluan --- plugins/capabilities/doc.go | 56 +++ plugins/capabilities/lifecycle.go | 32 ++ plugins/capabilities/metadata_agent.go | 173 +++++++ plugins/capabilities/scheduler_callback.go | 34 ++ plugins/capabilities/scrobbler.go | 115 +++++ plugins/capabilities/websocket_callback.go | 89 ++++ plugins/cmd/ndpgen/internal/generator.go | 98 ++++ plugins/cmd/ndpgen/internal/generator_test.go | 277 +++++++++++ plugins/cmd/ndpgen/internal/parser.go | 343 +++++++++++++ plugins/cmd/ndpgen/internal/parser_test.go | 251 ++++++++++ .../internal/templates/capability.go.tmpl | 150 ++++++ .../templates/capability_stub.go.tmpl | 95 ++++ plugins/cmd/ndpgen/internal/types.go | 97 ++++ plugins/cmd/ndpgen/main.go | 205 +++++++- plugins/examples/wikimedia/go.mod | 9 +- plugins/examples/wikimedia/main.go | 92 ++-- plugins/examples/wikimedia/pdk.gen.go | 376 -------------- plugins/pdk/go/lifecycle/lifecycle.go | 86 ++++ plugins/pdk/go/lifecycle/lifecycle_stub.go | 48 ++ plugins/pdk/go/metadata/metadata.go | 467 ++++++++++++++++++ plugins/pdk/go/metadata/metadata_stub.go | 212 ++++++++ plugins/pdk/go/scheduler/scheduler.go | 90 ++++ plugins/pdk/go/scheduler/scheduler_stub.go | 52 ++ plugins/pdk/go/scrobbler/scrobbler.go | 224 +++++++++ plugins/pdk/go/scrobbler/scrobbler_stub.go | 130 +++++ plugins/pdk/go/websocket/websocket.go | 247 +++++++++ plugins/pdk/go/websocket/websocket_stub.go | 116 +++++ 27 files changed, 3708 insertions(+), 456 deletions(-) create mode 100644 plugins/capabilities/doc.go create mode 100644 plugins/capabilities/lifecycle.go create mode 100644 plugins/capabilities/metadata_agent.go create mode 100644 plugins/capabilities/scheduler_callback.go create mode 100644 plugins/capabilities/scrobbler.go create mode 100644 plugins/capabilities/websocket_callback.go create mode 100644 plugins/cmd/ndpgen/internal/templates/capability.go.tmpl create mode 100644 plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl delete mode 100755 plugins/examples/wikimedia/pdk.gen.go create mode 100644 plugins/pdk/go/lifecycle/lifecycle.go create mode 100644 plugins/pdk/go/lifecycle/lifecycle_stub.go create mode 100644 plugins/pdk/go/metadata/metadata.go create mode 100644 plugins/pdk/go/metadata/metadata_stub.go create mode 100644 plugins/pdk/go/scheduler/scheduler.go create mode 100644 plugins/pdk/go/scheduler/scheduler_stub.go create mode 100644 plugins/pdk/go/scrobbler/scrobbler.go create mode 100644 plugins/pdk/go/scrobbler/scrobbler_stub.go create mode 100644 plugins/pdk/go/websocket/websocket.go create mode 100644 plugins/pdk/go/websocket/websocket_stub.go diff --git a/plugins/capabilities/doc.go b/plugins/capabilities/doc.go new file mode 100644 index 000000000..228eca20c --- /dev/null +++ b/plugins/capabilities/doc.go @@ -0,0 +1,56 @@ +// Package capabilities defines Go interfaces for Navidrome plugin capabilities. +// +// These interfaces serve as the source of truth for capability definitions. +// The ndpgen tool generates: +// - Go export wrappers in plugins/pdk/go// for Go plugins +// - XTP YAML schemas for non-Go plugins (Rust, TypeScript, etc.) +// +// Each capability is defined as an annotated interface: +// +// //nd:capability name=metadata +// type MetadataAgent interface { +// //nd:export name=nd_get_artist_biography +// GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error) +// } +// +// Annotation Reference: +// +// //nd:capability name= [required=true] +// - Marks an interface as a capability +// - name: Generated package name (e.g., name=metadata → pdk/go/metadata/) +// - required: If true, all methods must be implemented (default: false) +// +// //nd:export name= +// - Marks a method as an exported WASM function +// - name: The export name (e.g., nd_get_artist_biography) +// +// Generated Code Structure: +// +// For a capability like MetadataAgent with required=false: +// +// package metadata +// +// // Agent is the marker interface +// type Agent interface{} +// +// // Optional provider interfaces +// type ArtistBiographyProvider interface { +// GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error) +// } +// +// // Registration function +// func Register(impl Agent) { ... } +// +// For a capability with required=true: +// +// package scrobbler +// +// // Scrobbler requires all methods +// type Scrobbler interface { +// IsAuthorized(AuthInput) (AuthOutput, error) +// NowPlaying(NowPlayingInput) (ScrobblerOutput, error) +// Scrobble(ScrobbleInput) (ScrobblerOutput, error) +// } +// +// func Register(impl Scrobbler) { ... } +package capabilities diff --git a/plugins/capabilities/lifecycle.go b/plugins/capabilities/lifecycle.go new file mode 100644 index 000000000..5cbf0b542 --- /dev/null +++ b/plugins/capabilities/lifecycle.go @@ -0,0 +1,32 @@ +package capabilities + +// Lifecycle provides plugin lifecycle hooks. +// This capability allows plugins to perform initialization when loaded, +// such as establishing connections, starting background processes, or +// validating configuration. +// +// The OnInit function is called once when the plugin is loaded, and is NOT +// called when the plugin is hot-reloaded. Plugins should not assume this +// function will be called on every startup. +// +//nd:capability name=lifecycle +type Lifecycle interface { + // OnInit is called after a plugin is fully loaded with all services registered. + // Plugins can use this function to perform one-time initialization tasks. + // The output can contain an error string if initialization failed, which will be + // logged but will not prevent the plugin from being loaded. + //nd:export name=nd_on_init + OnInit(OnInitInput) (OnInitOutput, error) +} + +// OnInitInput is the input provided to the init callback. +// Currently empty, reserved for future use. +type OnInitInput struct{} + +// OnInitOutput is the output from the init callback. +type OnInitOutput struct { + // Error is the error message if initialization failed. + // Empty or null indicates success. + // The error is logged but does not prevent the plugin from being loaded. + Error *string `json:"error,omitempty"` +} diff --git a/plugins/capabilities/metadata_agent.go b/plugins/capabilities/metadata_agent.go new file mode 100644 index 000000000..bda349cf1 --- /dev/null +++ b/plugins/capabilities/metadata_agent.go @@ -0,0 +1,173 @@ +package capabilities + +// MetadataAgent provides artist and album metadata retrieval. +// This capability allows plugins to provide external metadata for artists and albums, +// such as biographies, images, similar artists, and top songs. +// +// Plugins implementing this capability can choose which methods to implement. +// Each method is optional - plugins only need to provide the functionality they support. +// +//nd:capability name=metadata +type MetadataAgent interface { + // GetArtistMBID retrieves the MusicBrainz ID for an artist. + //nd:export name=nd_get_artist_mbid + GetArtistMBID(ArtistMBIDInput) (ArtistMBIDOutput, error) + + // GetArtistURL retrieves the external URL for an artist. + //nd:export name=nd_get_artist_url + GetArtistURL(ArtistInput) (ArtistURLOutput, error) + + // GetArtistBiography retrieves the biography for an artist. + //nd:export name=nd_get_artist_biography + GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error) + + // GetSimilarArtists retrieves similar artists for a given artist. + //nd:export name=nd_get_similar_artists + GetSimilarArtists(SimilarArtistsInput) (SimilarArtistsOutput, error) + + // GetArtistImages retrieves images for an artist. + //nd:export name=nd_get_artist_images + GetArtistImages(ArtistInput) (ArtistImagesOutput, error) + + // GetArtistTopSongs retrieves top songs for an artist. + //nd:export name=nd_get_artist_top_songs + GetArtistTopSongs(TopSongsInput) (TopSongsOutput, error) + + // GetAlbumInfo retrieves album information. + //nd:export name=nd_get_album_info + GetAlbumInfo(AlbumInput) (AlbumInfoOutput, error) + + // GetAlbumImages retrieves images for an album. + //nd:export name=nd_get_album_images + GetAlbumImages(AlbumInput) (AlbumImagesOutput, error) +} + +// ArtistMBIDInput is the input for GetArtistMBID. +type ArtistMBIDInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` +} + +// ArtistMBIDOutput is the output for GetArtistMBID. +type ArtistMBIDOutput struct { + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid"` +} + +// ArtistInput is the common input for artist-related functions. +type ArtistInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` +} + +// ArtistURLOutput is the output for GetArtistURL. +type ArtistURLOutput struct { + // URL is the external URL for the artist. + URL string `json:"url"` +} + +// ArtistBiographyOutput is the output for GetArtistBiography. +type ArtistBiographyOutput struct { + // Biography is the artist biography text. + Biography string `json:"biography"` +} + +// SimilarArtistsInput is the input for GetSimilarArtists. +type SimilarArtistsInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` + // Limit is the maximum number of similar artists to return. + Limit int32 `json:"limit"` +} + +// ArtistRef is a reference to an artist with name and optional MBID. +type ArtistRef struct { + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist. + MBID *string `json:"mbid,omitempty"` +} + +// SimilarArtistsOutput is the output for GetSimilarArtists. +type SimilarArtistsOutput struct { + // Artists is the list of similar artists. + Artists []ArtistRef `json:"artists"` +} + +// ImageInfo represents an image with URL and size. +type ImageInfo struct { + // URL is the URL of the image. + URL string `json:"url"` + // Size is the size of the image in pixels (width or height). + Size int32 `json:"size"` +} + +// ArtistImagesOutput is the output for GetArtistImages. +type ArtistImagesOutput struct { + // Images is the list of artist images. + Images []ImageInfo `json:"images"` +} + +// TopSongsInput is the input for GetArtistTopSongs. +type TopSongsInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` + // Count is the maximum number of top songs to return. + Count int32 `json:"count"` +} + +// SongRef is a reference to a song with name and optional MBID. +type SongRef struct { + // Name is the song name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the song. + MBID *string `json:"mbid,omitempty"` +} + +// TopSongsOutput is the output for GetArtistTopSongs. +type TopSongsOutput struct { + // Songs is the list of top songs. + Songs []SongRef `json:"songs"` +} + +// AlbumInput is the common input for album-related functions. +type AlbumInput struct { + // Name is the album name. + Name string `json:"name"` + // Artist is the album artist name. + Artist string `json:"artist"` + // MBID is the MusicBrainz ID for the album (if known). + MBID *string `json:"mbid,omitempty"` +} + +// AlbumInfoOutput is the output for GetAlbumInfo. +type AlbumInfoOutput struct { + // Name is the album name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the album. + MBID string `json:"mbid"` + // Description is the album description/notes. + Description string `json:"description"` + // URL is the external URL for the album. + URL string `json:"url"` +} + +// AlbumImagesOutput is the output for GetAlbumImages. +type AlbumImagesOutput struct { + // Images is the list of album images. + Images []ImageInfo `json:"images"` +} diff --git a/plugins/capabilities/scheduler_callback.go b/plugins/capabilities/scheduler_callback.go new file mode 100644 index 000000000..78c178c92 --- /dev/null +++ b/plugins/capabilities/scheduler_callback.go @@ -0,0 +1,34 @@ +package capabilities + +// SchedulerCallback provides scheduled task handling. +// This capability allows plugins to receive callbacks when their scheduled tasks execute. +// Plugins that use the scheduler host service must implement this capability +// to handle task execution. +// +//nd:capability name=scheduler +type SchedulerCallback interface { + // OnSchedulerCallback is called when a scheduled task fires. + //nd:export name=nd_scheduler_callback + OnSchedulerCallback(SchedulerCallbackInput) (SchedulerCallbackOutput, error) +} + +// SchedulerCallbackInput is the input provided when a scheduled task fires. +type SchedulerCallbackInput struct { + // ScheduleID is the unique identifier for this scheduled task. + // This is either the ID provided when scheduling, or an auto-generated UUID if none was specified. + ScheduleID string `json:"scheduleId"` + // Payload is the payload data that was provided when the task was scheduled. + // Can be used to pass context or parameters to the callback handler. + Payload string `json:"payload"` + // IsRecurring is true if this is a recurring schedule (created via ScheduleRecurring), + // false if it's a one-time schedule (created via ScheduleOneTime). + IsRecurring bool `json:"isRecurring"` +} + +// SchedulerCallbackOutput is the output from the scheduler callback. +type SchedulerCallbackOutput struct { + // Error is the error message if the callback failed to process the scheduled task. + // Empty or null indicates success. The error is logged but does not + // affect the scheduling system. + Error *string `json:"error,omitempty"` +} diff --git a/plugins/capabilities/scrobbler.go b/plugins/capabilities/scrobbler.go new file mode 100644 index 000000000..17663a301 --- /dev/null +++ b/plugins/capabilities/scrobbler.go @@ -0,0 +1,115 @@ +package capabilities + +// Scrobbler provides scrobbling functionality to external services. +// This capability allows plugins to submit listening history to services like Last.fm, +// ListenBrainz, or custom scrobbling backends. +// +// All methods are required - plugins implementing this capability must provide +// all three functions: IsAuthorized, NowPlaying, and Scrobble. +// +//nd:capability name=scrobbler required=true +type Scrobbler interface { + // IsAuthorized checks if a user is authorized to scrobble to this service. + //nd:export name=nd_scrobbler_is_authorized + IsAuthorized(AuthInput) (AuthOutput, error) + + // NowPlaying sends a now playing notification to the scrobbling service. + //nd:export name=nd_scrobbler_now_playing + NowPlaying(NowPlayingInput) (ScrobblerOutput, error) + + // Scrobble submits a completed scrobble to the scrobbling service. + //nd:export name=nd_scrobbler_scrobble + Scrobble(ScrobbleInput) (ScrobblerOutput, error) +} + +// AuthInput is the input for authorization check. +type AuthInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` +} + +// AuthOutput is the output for authorization check. +type AuthOutput struct { + // Authorized indicates whether the user is authorized to scrobble. + Authorized bool `json:"authorized"` +} + +// TrackInfo contains track metadata for scrobbling. +type TrackInfo struct { + // ID is the internal Navidrome track ID. + ID string `json:"id"` + // Title is the track title. + Title string `json:"title"` + // Album is the album name. + Album string `json:"album"` + // Artist is the track artist. + Artist string `json:"artist"` + // AlbumArtist is the album artist. + AlbumArtist string `json:"albumArtist"` + // Duration is the track duration in seconds. + Duration float64 `json:"duration"` + // TrackNumber is the track number on the album. + TrackNumber int32 `json:"trackNumber"` + // DiscNumber is the disc number. + DiscNumber int32 `json:"discNumber"` + // MBZRecordingID is the MusicBrainz recording ID. + MBZRecordingID *string `json:"mbzRecordingId,omitempty"` + // MBZAlbumID is the MusicBrainz album/release ID. + MBZAlbumID *string `json:"mbzAlbumId,omitempty"` + // MBZArtistID is the MusicBrainz artist ID. + MBZArtistID *string `json:"mbzArtistId,omitempty"` + // MBZReleaseGroupID is the MusicBrainz release group ID. + MBZReleaseGroupID *string `json:"mbzReleaseGroupId,omitempty"` + // MBZAlbumArtistID is the MusicBrainz album artist ID. + MBZAlbumArtistID *string `json:"mbzAlbumArtistId,omitempty"` + // MBZReleaseTrackID is the MusicBrainz release track ID. + MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"` +} + +// NowPlayingInput is the input for now playing notification. +type NowPlayingInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` + // Track is the track currently playing. + Track TrackInfo `json:"track"` + // Position is the current playback position in seconds. + Position int32 `json:"position"` +} + +// ScrobbleInput is the input for submitting a scrobble. +type ScrobbleInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` + // Track is the track that was played. + Track TrackInfo `json:"track"` + // Timestamp is the Unix timestamp when the track started playing. + Timestamp int64 `json:"timestamp"` +} + +// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors. +type ScrobblerErrorType string + +const ( + // ScrobblerErrorNone indicates no error occurred. + ScrobblerErrorNone ScrobblerErrorType = "none" + // ScrobblerErrorNotAuthorized indicates the user is not authorized. + ScrobblerErrorNotAuthorized ScrobblerErrorType = "not_authorized" + // ScrobblerErrorRetryLater indicates the operation should be retried later. + ScrobblerErrorRetryLater ScrobblerErrorType = "retry_later" + // ScrobblerErrorUnrecoverable indicates an unrecoverable error. + ScrobblerErrorUnrecoverable ScrobblerErrorType = "unrecoverable" +) + +// ScrobblerOutput is the output for scrobbler operations. +type ScrobblerOutput struct { + // Error is the error message if the operation failed. + Error *string `json:"error,omitempty"` + // ErrorType indicates how Navidrome should handle the error. + ErrorType *ScrobblerErrorType `json:"errorType,omitempty"` +} diff --git a/plugins/capabilities/websocket_callback.go b/plugins/capabilities/websocket_callback.go new file mode 100644 index 000000000..f9cd6333f --- /dev/null +++ b/plugins/capabilities/websocket_callback.go @@ -0,0 +1,89 @@ +package capabilities + +// WebSocketCallback provides WebSocket message handling. +// This capability allows plugins to receive callbacks for WebSocket events +// such as text messages, binary messages, errors, and connection closures. +// Plugins that use the WebSocket host service must implement this capability +// to handle incoming events. +// +//nd:capability name=websocket +type WebSocketCallback interface { + // OnTextMessage is called when a text message is received on a WebSocket connection. + //nd:export name=nd_websocket_on_text_message + OnTextMessage(OnTextMessageInput) (OnTextMessageOutput, error) + + // OnBinaryMessage is called when a binary message is received on a WebSocket connection. + //nd:export name=nd_websocket_on_binary_message + OnBinaryMessage(OnBinaryMessageInput) (OnBinaryMessageOutput, error) + + // OnError is called when an error occurs on a WebSocket connection. + //nd:export name=nd_websocket_on_error + OnError(OnErrorInput) (OnErrorOutput, error) + + // OnClose is called when a WebSocket connection is closed. + //nd:export name=nd_websocket_on_close + OnClose(OnCloseInput) (OnCloseOutput, error) +} + +// OnTextMessageInput is the input provided when a text message is received. +type OnTextMessageInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that received the message. + ConnectionID string `json:"connectionId"` + // Message is the text message content received from the WebSocket. + Message string `json:"message"` +} + +// OnTextMessageOutput is the output from the text message handler. +type OnTextMessageOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnBinaryMessageInput is the input provided when a binary message is received. +type OnBinaryMessageInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that received the message. + ConnectionID string `json:"connectionId"` + // Data is the binary data received from the WebSocket, encoded as base64. + Data string `json:"data"` +} + +// OnBinaryMessageOutput is the output from the binary message handler. +type OnBinaryMessageOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnErrorInput is the input provided when an error occurs on a WebSocket connection. +type OnErrorInput struct { + // ConnectionID is the unique identifier for the WebSocket connection where the error occurred. + ConnectionID string `json:"connectionId"` + // Error is the error message describing what went wrong. + Error string `json:"error"` +} + +// OnErrorOutput is the output from the error handler. +type OnErrorOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnCloseInput is the input provided when a WebSocket connection is closed. +type OnCloseInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that was closed. + ConnectionID string `json:"connectionId"` + // Code is the WebSocket close status code (e.g., 1000 for normal closure, + // 1001 for going away, 1006 for abnormal closure). + Code int32 `json:"code"` + // Reason is the human-readable reason for the connection closure, if provided. + Reason string `json:"reason"` +} + +// OnCloseOutput is the output from the close handler. +type OnCloseOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} diff --git a/plugins/cmd/ndpgen/internal/generator.go b/plugins/cmd/ndpgen/internal/generator.go index 4f1c6e44b..87045627e 100644 --- a/plugins/cmd/ndpgen/internal/generator.go +++ b/plugins/cmd/ndpgen/internal/generator.go @@ -303,3 +303,101 @@ func GenerateGoMod() ([]byte, error) { } return tmplContent, nil } + +// capabilityTemplateData holds data for capability template execution. +type capabilityTemplateData struct { + Package string + Capability Capability +} + +// capabilityFuncMap returns template functions for capability code generation. +func capabilityFuncMap(cap Capability) template.FuncMap { + return template.FuncMap{ + "formatDoc": formatDoc, + "indent": indentText, + "agentName": capabilityAgentName, + "providerInterface": func(e Export) string { return e.ProviderInterfaceName() }, + "implVar": func(e Export) string { return e.ImplVarName() }, + "exportFunc": func(e Export) string { return e.ExportFuncName() }, + } +} + +// indentText adds n tabs to each line of text. +func indentText(n int, s string) string { + indent := strings.Repeat("\t", n) + lines := strings.Split(s, "\n") + for i, line := range lines { + if line != "" { + lines[i] = indent + line + } + } + return strings.Join(lines, "\n") +} + +// capabilityAgentName returns the interface name for a capability. +// Uses the Go interface name stripped of common suffixes. +func capabilityAgentName(cap Capability) string { + name := cap.Interface + // Remove common suffixes to get a clean name + for _, suffix := range []string{"Agent", "Callback", "Service"} { + if strings.HasSuffix(name, suffix) { + name = name[:len(name)-len(suffix)] + break + } + } + // Use the shortened name or the original if no suffix found + if name == "" { + name = cap.Interface + } + return name +} + +// GenerateCapabilityGo generates Go export wrapper code for a capability. +func GenerateCapabilityGo(cap Capability, pkgName string) ([]byte, error) { + tmplContent, err := templatesFS.ReadFile("templates/capability.go.tmpl") + if err != nil { + return nil, fmt.Errorf("reading capability template: %w", err) + } + + tmpl, err := template.New("capability").Funcs(capabilityFuncMap(cap)).Parse(string(tmplContent)) + if err != nil { + return nil, fmt.Errorf("parsing template: %w", err) + } + + data := capabilityTemplateData{ + Package: pkgName, + Capability: cap, + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return nil, fmt.Errorf("executing template: %w", err) + } + + return buf.Bytes(), nil +} + +// GenerateCapabilityGoStub generates stub code for non-WASM platforms. +func GenerateCapabilityGoStub(cap Capability, pkgName string) ([]byte, error) { + tmplContent, err := templatesFS.ReadFile("templates/capability_stub.go.tmpl") + if err != nil { + return nil, fmt.Errorf("reading capability stub template: %w", err) + } + + tmpl, err := template.New("capability_stub").Funcs(capabilityFuncMap(cap)).Parse(string(tmplContent)) + if err != nil { + return nil, fmt.Errorf("parsing template: %w", err) + } + + data := capabilityTemplateData{ + Package: pkgName, + Capability: cap, + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return nil, fmt.Errorf("executing template: %w", err) + } + + return buf.Bytes(), nil +} diff --git a/plugins/cmd/ndpgen/internal/generator_test.go b/plugins/cmd/ndpgen/internal/generator_test.go index 8cf0a65ca..73b40e30b 100644 --- a/plugins/cmd/ndpgen/internal/generator_test.go +++ b/plugins/cmd/ndpgen/internal/generator_test.go @@ -672,6 +672,283 @@ type TestService interface { Expect(codeStr).To(ContainSubstring(`"test_dosomething"`)) }) }) + + Describe("GenerateCapabilityGo", func() { + It("should generate valid Go code for a non-required capability", func() { + cap := Capability{ + Name: "metadata", + Interface: "MetadataAgent", + Required: false, + Doc: "MetadataAgent provides metadata retrieval.", + Methods: []Export{ + { + Name: "GetArtistBiography", + ExportName: "nd_get_artist_biography", + Input: Param{Type: "ArtistInput"}, + Output: Param{Type: "ArtistBiographyOutput"}, + Doc: "Returns artist biography", + }, + { + Name: "GetArtistImages", + ExportName: "nd_get_artist_images", + Input: Param{Type: "ArtistInput"}, + Output: Param{Type: "ArtistImagesOutput"}, + Doc: "Returns artist images", + }, + }, + Structs: []StructDef{ + { + Name: "ArtistInput", + Fields: []FieldDef{ + {Name: "ID", Type: "string", JSONTag: "id"}, + {Name: "Name", Type: "string", JSONTag: "name"}, + }, + }, + { + Name: "ArtistBiographyOutput", + Fields: []FieldDef{ + {Name: "Biography", Type: "string", JSONTag: "biography"}, + }, + }, + { + Name: "ArtistImagesOutput", + Fields: []FieldDef{ + {Name: "Images", Type: "[]ImageInfo", JSONTag: "images"}, + }, + }, + { + Name: "ImageInfo", + Fields: []FieldDef{ + {Name: "URL", Type: "string", JSONTag: "url"}, + {Name: "Size", Type: "int32", JSONTag: "size"}, + }, + }, + }, + } + + code, err := GenerateCapabilityGo(cap, "metadata") + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + + // Check for build tag + Expect(codeStr).To(ContainSubstring("//go:build wasip1")) + + // Check for package declaration + Expect(codeStr).To(ContainSubstring("package metadata")) + + // Check for marker interface (non-required) + Expect(codeStr).To(ContainSubstring("type Metadata interface{}")) + + // Check for provider interfaces + Expect(codeStr).To(ContainSubstring("type ArtistBiographyProvider interface")) + Expect(codeStr).To(ContainSubstring("type ArtistImagesProvider interface")) + + // Check for Register function with type assertions + Expect(codeStr).To(ContainSubstring("func Register(impl Metadata)")) + Expect(codeStr).To(ContainSubstring("impl.(ArtistBiographyProvider)")) + + // Check for export wrappers + Expect(codeStr).To(ContainSubstring("//export nd_get_artist_biography")) + Expect(codeStr).To(ContainSubstring("func _NdGetArtistBiography()")) + + // Check for NotImplementedCode handling + Expect(codeStr).To(ContainSubstring("NotImplementedCode")) + Expect(codeStr).To(ContainSubstring("return NotImplementedCode")) + + // Check struct definitions + Expect(codeStr).To(ContainSubstring("type ArtistInput struct")) + Expect(codeStr).To(ContainSubstring("type ImageInfo struct")) + }) + + It("should generate valid Go code for a required capability", func() { + cap := Capability{ + Name: "scrobbler", + Interface: "Scrobbler", + Required: true, + Methods: []Export{ + { + Name: "IsAuthorized", + ExportName: "nd_scrobbler_is_authorized", + Input: Param{Type: "AuthInput"}, + Output: Param{Type: "AuthOutput"}, + }, + { + Name: "Scrobble", + ExportName: "nd_scrobbler_scrobble", + Input: Param{Type: "ScrobbleInput"}, + Output: Param{Type: "ScrobblerOutput"}, + }, + }, + Structs: []StructDef{ + {Name: "AuthInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}}, + {Name: "AuthOutput", Fields: []FieldDef{{Name: "Authorized", Type: "bool", JSONTag: "authorized"}}}, + {Name: "ScrobbleInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}}, + {Name: "ScrobblerOutput", Fields: []FieldDef{{Name: "Error", Type: "*string", JSONTag: "error", OmitEmpty: true}}}, + }, + } + + code, err := GenerateCapabilityGo(cap, "scrobbler") + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + + // Check for full interface (required capability) + Expect(codeStr).To(ContainSubstring("type Scrobbler interface {")) + Expect(codeStr).To(ContainSubstring("IsAuthorized(AuthInput) (AuthOutput, error)")) + Expect(codeStr).To(ContainSubstring("Scrobble(ScrobbleInput) (ScrobblerOutput, error)")) + + // Should NOT have provider interfaces for required capability + Expect(codeStr).NotTo(ContainSubstring("AuthProvider interface")) + + // Register should directly assign methods + Expect(codeStr).To(ContainSubstring("func Register(impl Scrobbler)")) + Expect(codeStr).To(ContainSubstring("impl.IsAuthorized")) + }) + + It("should include type aliases and consts", func() { + cap := Capability{ + Name: "scrobbler", + Interface: "Scrobbler", + Required: true, + Methods: []Export{ + { + Name: "Scrobble", + ExportName: "nd_scrobble", + Input: Param{Type: "ScrobbleInput"}, + Output: Param{Type: "ScrobblerOutput"}, + }, + }, + Structs: []StructDef{ + {Name: "ScrobbleInput", Fields: []FieldDef{{Name: "UserID", Type: "string", JSONTag: "userId"}}}, + {Name: "ScrobblerOutput", Fields: []FieldDef{{Name: "ErrorType", Type: "*ScrobblerErrorType", JSONTag: "errorType", OmitEmpty: true}}}, + }, + TypeAliases: []TypeAlias{ + {Name: "ScrobblerErrorType", Type: "string", Doc: "ScrobblerErrorType indicates error handling."}, + }, + Consts: []ConstGroup{ + { + Type: "ScrobblerErrorType", + Values: []ConstDef{ + {Name: "ScrobblerErrorNone", Value: `"none"`, Doc: "No error"}, + {Name: "ScrobblerErrorRetry", Value: `"retry"`, Doc: "Retry later"}, + }, + }, + }, + } + + code, err := GenerateCapabilityGo(cap, "scrobbler") + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + + // Check type alias + Expect(codeStr).To(ContainSubstring("type ScrobblerErrorType string")) + + // Check consts + Expect(codeStr).To(ContainSubstring("ScrobblerErrorNone")) + Expect(codeStr).To(ContainSubstring(`"none"`)) + Expect(codeStr).To(ContainSubstring("ScrobblerErrorRetry")) + }) + }) + + Describe("GenerateCapabilityGoStub", func() { + It("should generate valid stub code for non-WASM builds", func() { + cap := Capability{ + Name: "metadata", + Interface: "MetadataAgent", + Required: false, + Methods: []Export{ + { + Name: "GetArtistBiography", + ExportName: "nd_get_artist_biography", + Input: Param{Type: "ArtistInput"}, + Output: Param{Type: "ArtistBiographyOutput"}, + }, + }, + Structs: []StructDef{ + {Name: "ArtistInput", Fields: []FieldDef{{Name: "ID", Type: "string", JSONTag: "id"}}}, + {Name: "ArtistBiographyOutput", Fields: []FieldDef{{Name: "Biography", Type: "string", JSONTag: "biography"}}}, + }, + } + + code, err := GenerateCapabilityGoStub(cap, "metadata") + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + + // Check for non-WASM build tag + Expect(codeStr).To(ContainSubstring("//go:build !wasip1")) + + // Check for package declaration + Expect(codeStr).To(ContainSubstring("package metadata")) + + // Check for no-op Register + Expect(codeStr).To(ContainSubstring("func Register(_ Metadata) {}")) + + // Check struct definitions are present + Expect(codeStr).To(ContainSubstring("type ArtistInput struct")) + + // Check there are no export wrappers + Expect(codeStr).NotTo(ContainSubstring("//export")) + Expect(codeStr).NotTo(ContainSubstring("pdk.InputJSON")) + }) + }) + + Describe("End-to-end capability generation", func() { + It("should parse and generate capability code from source", func() { + src := `package capabilities + +// Lifecycle provides plugin lifecycle hooks. +//nd:capability name=lifecycle +type Lifecycle interface { + // OnInit is called when the plugin is loaded. + //nd:export name=nd_on_init + OnInit(OnInitInput) (OnInitOutput, error) +} + +// OnInitInput is the input for OnInit. +type OnInitInput struct { +} + +// OnInitOutput is the output for OnInit. +type OnInitOutput struct { + // Error is the error message if initialization failed. + Error *string ` + "`json:\"error,omitempty\"`" + ` +} +` + // Create temporary directory + tmpDir := GinkgoT().TempDir() + path := tmpDir + "/lifecycle.go" + err := writeFile(path, src) + Expect(err).NotTo(HaveOccurred()) + + // Parse + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(HaveLen(1)) + + cap := capabilities[0] + Expect(cap.Name).To(Equal("lifecycle")) + Expect(cap.Methods).To(HaveLen(1)) + + // Generate WASM code + code, err := GenerateCapabilityGo(cap, "lifecycle") + Expect(err).NotTo(HaveOccurred()) + + codeStr := string(code) + Expect(codeStr).To(ContainSubstring("//export nd_on_init")) + Expect(codeStr).To(ContainSubstring("type InitProvider interface")) + + // Generate stub code + stubCode, err := GenerateCapabilityGoStub(cap, "lifecycle") + Expect(err).NotTo(HaveOccurred()) + + stubStr := string(stubCode) + Expect(stubStr).To(ContainSubstring("//go:build !wasip1")) + Expect(stubStr).To(ContainSubstring("func Register(_ Lifecycle) {}")) + }) + }) }) func writeFile(path, content string) error { diff --git a/plugins/cmd/ndpgen/internal/parser.go b/plugins/cmd/ndpgen/internal/parser.go index 48ffdeb0e..9f420b32d 100644 --- a/plugins/cmd/ndpgen/internal/parser.go +++ b/plugins/cmd/ndpgen/internal/parser.go @@ -17,6 +17,10 @@ var ( hostServicePattern = regexp.MustCompile(`//nd:hostservice\s+(.*)`) // //nd:hostfunc [name=CustomName] hostFuncPattern = regexp.MustCompile(`//nd:hostfunc(?:\s+(.*))?`) + // //nd:capability name=PackageName [required=true] + capabilityPattern = regexp.MustCompile(`//nd:capability\s+(.*)`) + // //nd:export name=ExportName + exportPattern = regexp.MustCompile(`//nd:export\s+(.*)`) // key=value pairs keyValuePattern = regexp.MustCompile(`(\w+)=(\S+)`) ) @@ -51,6 +55,223 @@ func ParseDirectory(dir string) ([]Service, error) { return services, nil } +// ParseCapabilities parses all Go source files in a directory and extracts capabilities. +func ParseCapabilities(dir string) ([]Capability, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return nil, fmt.Errorf("reading directory: %w", err) + } + + var capabilities []Capability + fset := token.NewFileSet() + + for _, entry := range entries { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { + continue + } + // Skip generated files, test files, and doc.go + if strings.HasSuffix(entry.Name(), "_gen.go") || + strings.HasSuffix(entry.Name(), "_test.go") || + entry.Name() == "doc.go" { + continue + } + + path := filepath.Join(dir, entry.Name()) + parsed, err := parseCapabilityFile(fset, path) + if err != nil { + return nil, fmt.Errorf("parsing %s: %w", entry.Name(), err) + } + capabilities = append(capabilities, parsed...) + } + + return capabilities, nil +} + +// parseCapabilityFile parses a single Go source file and extracts capabilities. +func parseCapabilityFile(fset *token.FileSet, path string) ([]Capability, error) { + f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + // First pass: collect all struct definitions in the file + allStructs := parseStructs(f) + structMap := make(map[string]StructDef) + for _, s := range allStructs { + structMap[s.Name] = s + } + + // Collect type aliases and consts + allTypeAliases := parseTypeAliases(f) + aliasMap := make(map[string]TypeAlias) + for _, a := range allTypeAliases { + aliasMap[a.Name] = a + } + allConstGroups := parseConstGroups(f) + + var capabilities []Capability + + for _, decl := range f.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + interfaceType, ok := typeSpec.Type.(*ast.InterfaceType) + if !ok { + continue + } + + // Check for //nd:capability annotation in doc comment + docText, rawDoc := getDocComment(genDecl, typeSpec) + capAnnotation := parseCapabilityAnnotation(rawDoc) + if capAnnotation == nil { + continue + } + + capability := Capability{ + Name: capAnnotation["name"], + Interface: typeSpec.Name.Name, + Required: capAnnotation["required"] == "true", + Doc: cleanDoc(docText), + } + + // Parse methods and collect referenced types + referencedTypes := make(map[string]bool) + for _, method := range interfaceType.Methods.List { + if len(method.Names) == 0 { + continue // Embedded interface + } + + funcType, ok := method.Type.(*ast.FuncType) + if !ok { + continue + } + + // Check for //nd:export annotation + methodDocText, methodRawDoc := getMethodDocComment(method) + exportAnnotation := parseExportAnnotation(methodRawDoc) + if exportAnnotation == nil { + continue + } + + export, err := parseExport(method.Names[0].Name, funcType, exportAnnotation, cleanDoc(methodDocText)) + if err != nil { + return nil, fmt.Errorf("parsing export %s.%s: %w", typeSpec.Name.Name, method.Names[0].Name, err) + } + capability.Methods = append(capability.Methods, export) + + // Collect referenced types from input and output + collectReferencedTypes(export.Input.Type, referencedTypes) + collectReferencedTypes(export.Output.Type, referencedTypes) + } + + // Recursively collect all struct dependencies + collectAllStructDependencies(referencedTypes, structMap) + + // Attach referenced structs to the capability + for typeName := range referencedTypes { + if s, exists := structMap[typeName]; exists { + capability.Structs = append(capability.Structs, s) + } + } + + // Attach referenced type aliases + for typeName := range referencedTypes { + if a, exists := aliasMap[typeName]; exists { + capability.TypeAliases = append(capability.TypeAliases, a) + } + } + + // Attach const groups that match referenced type aliases + for _, group := range allConstGroups { + if group.Type == "" { + continue + } + if referencedTypes[group.Type] { + capability.Consts = append(capability.Consts, group) + } + } + + if len(capability.Methods) > 0 { + capabilities = append(capabilities, capability) + } + } + } + + return capabilities, nil +} + +// collectAllStructDependencies recursively collects all struct types referenced by other structs. +func collectAllStructDependencies(referencedTypes map[string]bool, structMap map[string]StructDef) { + // Keep iterating until no new types are added + for { + newTypes := make(map[string]bool) + for typeName := range referencedTypes { + if s, exists := structMap[typeName]; exists { + for _, field := range s.Fields { + collectReferencedTypes(field.Type, newTypes) + } + } + } + // Check if any new types were found + foundNew := false + for t := range newTypes { + if !referencedTypes[t] { + referencedTypes[t] = true + foundNew = true + } + } + if !foundNew { + break + } + } +} + +// parseExport parses an export method signature into an Export struct. +func parseExport(name string, funcType *ast.FuncType, annotation map[string]string, doc string) (Export, error) { + export := Export{ + Name: name, + ExportName: annotation["name"], + Doc: doc, + } + + // Capability exports have exactly one input parameter (the struct type) + if funcType.Params != nil && len(funcType.Params.List) == 1 { + field := funcType.Params.List[0] + typeName := typeToString(field.Type) + paramName := "input" + if len(field.Names) > 0 { + paramName = field.Names[0].Name + } + export.Input = NewParam(paramName, typeName) + } + + // Capability exports return (OutputType, error) + if funcType.Results != nil { + for _, field := range funcType.Results.List { + typeName := typeToString(field.Type) + if typeName == "error" { + continue // Skip error return + } + paramName := "output" + if len(field.Names) > 0 { + paramName = field.Names[0].Name + } + export.Output = NewParam(paramName, typeName) + break // Only take the first non-error return + } + } + + return export, nil +} + // parseFile parses a single Go source file and extracts host services. func parseFile(fset *token.FileSet, path string) ([]Service, error) { f, err := parser.ParseFile(fset, path, nil, parser.ParseComments) @@ -192,6 +413,104 @@ func parseStructs(f *ast.File) []StructDef { return structs } +// parseTypeAliases extracts all type alias definitions from a parsed Go file. +// Type aliases are non-struct type declarations like: type MyType string +func parseTypeAliases(f *ast.File) []TypeAlias { + var aliases []TypeAlias + + for _, decl := range f.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + + for _, spec := range genDecl.Specs { + typeSpec, ok := spec.(*ast.TypeSpec) + if !ok { + continue + } + + // Skip struct and interface types + if _, isStruct := typeSpec.Type.(*ast.StructType); isStruct { + continue + } + if _, isInterface := typeSpec.Type.(*ast.InterfaceType); isInterface { + continue + } + + docText, _ := getDocComment(genDecl, typeSpec) + aliases = append(aliases, TypeAlias{ + Name: typeSpec.Name.Name, + Type: typeToString(typeSpec.Type), + Doc: cleanDoc(docText), + }) + } + } + + return aliases +} + +// parseConstGroups extracts const groups from a parsed Go file. +func parseConstGroups(f *ast.File) []ConstGroup { + var groups []ConstGroup + + for _, decl := range f.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.CONST { + continue + } + + group := ConstGroup{} + for _, spec := range genDecl.Specs { + valueSpec, ok := spec.(*ast.ValueSpec) + if !ok { + continue + } + + // Get type if specified + if valueSpec.Type != nil && group.Type == "" { + group.Type = typeToString(valueSpec.Type) + } + + // Extract values + for i, name := range valueSpec.Names { + def := ConstDef{ + Name: name.Name, + } + // Get value if present + if i < len(valueSpec.Values) { + def.Value = exprToString(valueSpec.Values[i]) + } + // Get doc comment + if valueSpec.Doc != nil { + def.Doc = cleanDoc(valueSpec.Doc.Text()) + } else if valueSpec.Comment != nil { + def.Doc = cleanDoc(valueSpec.Comment.Text()) + } + group.Values = append(group.Values, def) + } + } + + if len(group.Values) > 0 { + groups = append(groups, group) + } + } + + return groups +} + +// exprToString converts an AST expression to a Go source string. +func exprToString(expr ast.Expr) string { + switch e := expr.(type) { + case *ast.BasicLit: + return e.Value + case *ast.Ident: + return e.Name + default: + return "" + } +} + // parseStructField parses a struct field and returns FieldDef for each name. func parseStructField(field *ast.Field) []FieldDef { var fields []FieldDef @@ -371,6 +690,30 @@ func parseHostFuncAnnotation(doc string) map[string]string { return nil } +// parseCapabilityAnnotation extracts //nd:capability annotation parameters. +func parseCapabilityAnnotation(doc string) map[string]string { + for _, line := range strings.Split(doc, "\n") { + line = strings.TrimSpace(line) + match := capabilityPattern.FindStringSubmatch(line) + if match != nil { + return parseKeyValuePairs(match[1]) + } + } + return nil +} + +// parseExportAnnotation extracts //nd:export annotation parameters. +func parseExportAnnotation(doc string) map[string]string { + for _, line := range strings.Split(doc, "\n") { + line = strings.TrimSpace(line) + match := exportPattern.FindStringSubmatch(line) + if match != nil { + return parseKeyValuePairs(match[1]) + } + } + return nil +} + // parseKeyValuePairs extracts key=value pairs from annotation text. func parseKeyValuePairs(text string) map[string]string { matches := keyValuePattern.FindAllStringSubmatch(text, -1) diff --git a/plugins/cmd/ndpgen/internal/parser_test.go b/plugins/cmd/ndpgen/internal/parser_test.go index 98c2cfb58..90edec036 100644 --- a/plugins/cmd/ndpgen/internal/parser_test.go +++ b/plugins/cmd/ndpgen/internal/parser_test.go @@ -289,4 +289,255 @@ type TestService interface { Expect(s.ExportPrefix()).To(Equal("subsonicapi")) }) }) + + Describe("ParseCapabilities", func() { + It("should parse a simple capability interface", func() { + src := `package capabilities + +// MetadataAgent provides metadata retrieval. +//nd:capability name=metadata +type MetadataAgent interface { + // GetArtistBiography returns artist biography. + //nd:export name=nd_get_artist_biography + GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error) +} + +// ArtistInput is the input for artist-related functions. +type ArtistInput struct { + // ID is the artist ID. + ID string ` + "`json:\"id\"`" + ` + // Name is the artist name. + Name string ` + "`json:\"name\"`" + ` +} + +// ArtistBiographyOutput is the output for GetArtistBiography. +type ArtistBiographyOutput struct { + // Biography is the biography text. + Biography string ` + "`json:\"biography\"`" + ` +} +` + err := os.WriteFile(filepath.Join(tmpDir, "metadata.go"), []byte(src), 0600) + Expect(err).NotTo(HaveOccurred()) + + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(HaveLen(1)) + + cap := capabilities[0] + Expect(cap.Name).To(Equal("metadata")) + Expect(cap.Interface).To(Equal("MetadataAgent")) + Expect(cap.Required).To(BeFalse()) + Expect(cap.Doc).To(ContainSubstring("MetadataAgent provides metadata retrieval")) + Expect(cap.Methods).To(HaveLen(1)) + + m := cap.Methods[0] + Expect(m.Name).To(Equal("GetArtistBiography")) + Expect(m.ExportName).To(Equal("nd_get_artist_biography")) + Expect(m.Input.Type).To(Equal("ArtistInput")) + Expect(m.Output.Type).To(Equal("ArtistBiographyOutput")) + + // Check structs were collected + Expect(cap.Structs).To(HaveLen(2)) + }) + + It("should parse a required capability", func() { + src := `package capabilities + +// Scrobbler requires all methods to be implemented. +//nd:capability name=scrobbler required=true +type Scrobbler interface { + //nd:export name=nd_scrobbler_is_authorized + IsAuthorized(AuthInput) (AuthOutput, error) + + //nd:export name=nd_scrobbler_scrobble + Scrobble(ScrobbleInput) (ScrobblerOutput, error) +} + +type AuthInput struct { + UserID string ` + "`json:\"userId\"`" + ` +} + +type AuthOutput struct { + Authorized bool ` + "`json:\"authorized\"`" + ` +} + +type ScrobbleInput struct { + UserID string ` + "`json:\"userId\"`" + ` +} + +type ScrobblerOutput struct { + Error *string ` + "`json:\"error,omitempty\"`" + ` +} +` + err := os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600) + Expect(err).NotTo(HaveOccurred()) + + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(HaveLen(1)) + + cap := capabilities[0] + Expect(cap.Name).To(Equal("scrobbler")) + Expect(cap.Required).To(BeTrue()) + Expect(cap.Methods).To(HaveLen(2)) + }) + + It("should parse type aliases and consts", func() { + src := `package capabilities + +//nd:capability name=scrobbler required=true +type Scrobbler interface { + //nd:export name=nd_scrobble + Scrobble(ScrobbleInput) (ScrobblerOutput, error) +} + +type ScrobbleInput struct { + UserID string ` + "`json:\"userId\"`" + ` +} + +// ScrobblerErrorType indicates error handling behavior. +type ScrobblerErrorType string + +const ( + // ScrobblerErrorNone indicates no error. + ScrobblerErrorNone ScrobblerErrorType = "none" + // ScrobblerErrorRetry indicates retry later. + ScrobblerErrorRetry ScrobblerErrorType = "retry" +) + +type ScrobblerOutput struct { + ErrorType *ScrobblerErrorType ` + "`json:\"errorType,omitempty\"`" + ` +} +` + err := os.WriteFile(filepath.Join(tmpDir, "scrobbler.go"), []byte(src), 0600) + Expect(err).NotTo(HaveOccurred()) + + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(HaveLen(1)) + + cap := capabilities[0] + // Type alias should be collected + Expect(cap.TypeAliases).To(HaveLen(1)) + Expect(cap.TypeAliases[0].Name).To(Equal("ScrobblerErrorType")) + Expect(cap.TypeAliases[0].Type).To(Equal("string")) + + // Consts should be collected + Expect(cap.Consts).To(HaveLen(1)) + Expect(cap.Consts[0].Type).To(Equal("ScrobblerErrorType")) + Expect(cap.Consts[0].Values).To(HaveLen(2)) + Expect(cap.Consts[0].Values[0].Name).To(Equal("ScrobblerErrorNone")) + Expect(cap.Consts[0].Values[0].Value).To(Equal(`"none"`)) + }) + + It("should collect nested struct dependencies", func() { + src := `package capabilities + +//nd:capability name=metadata +type MetadataAgent interface { + //nd:export name=nd_get_images + GetImages(ArtistInput) (ImagesOutput, error) +} + +type ArtistInput struct { + ID string ` + "`json:\"id\"`" + ` +} + +type ImagesOutput struct { + Images []ImageInfo ` + "`json:\"images\"`" + ` +} + +type ImageInfo struct { + URL string ` + "`json:\"url\"`" + ` + Size int32 ` + "`json:\"size\"`" + ` +} +` + err := os.WriteFile(filepath.Join(tmpDir, "metadata.go"), []byte(src), 0600) + Expect(err).NotTo(HaveOccurred()) + + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(HaveLen(1)) + + cap := capabilities[0] + // Should collect all 3 structs: ArtistInput, ImagesOutput, and ImageInfo + Expect(cap.Structs).To(HaveLen(3)) + + structNames := make([]string, len(cap.Structs)) + for i, s := range cap.Structs { + structNames[i] = s.Name + } + Expect(structNames).To(ContainElements("ArtistInput", "ImagesOutput", "ImageInfo")) + }) + + It("should return empty slice for directory with no capabilities", func() { + src := `package capabilities + +type RegularInterface interface { + Method() error +} +` + err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600) + Expect(err).NotTo(HaveOccurred()) + + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(BeEmpty()) + }) + + It("should ignore methods without export annotation", func() { + src := `package capabilities + +//nd:capability name=test +type TestCapability interface { + //nd:export name=nd_exported + ExportedMethod(Input) (Output, error) + + // This method has no export annotation + NotExportedMethod(Input) (Output, error) +} + +type Input struct { + Value string ` + "`json:\"value\"`" + ` +} + +type Output struct { + Result string ` + "`json:\"result\"`" + ` +} +` + err := os.WriteFile(filepath.Join(tmpDir, "test.go"), []byte(src), 0600) + Expect(err).NotTo(HaveOccurred()) + + capabilities, err := ParseCapabilities(tmpDir) + Expect(err).NotTo(HaveOccurred()) + Expect(capabilities).To(HaveLen(1)) + + // Only the exported method should be captured + Expect(capabilities[0].Methods).To(HaveLen(1)) + Expect(capabilities[0].Methods[0].Name).To(Equal("ExportedMethod")) + }) + }) + + Describe("Export helpers", func() { + It("should generate correct provider interface name", func() { + e := Export{Name: "GetArtistBiography"} + Expect(e.ProviderInterfaceName()).To(Equal("ArtistBiographyProvider")) + + e = Export{Name: "OnInit"} + Expect(e.ProviderInterfaceName()).To(Equal("InitProvider")) + }) + + It("should generate correct impl variable name", func() { + e := Export{Name: "GetArtistBiography"} + Expect(e.ImplVarName()).To(Equal("artistBiographyImpl")) + + e = Export{Name: "OnInit"} + Expect(e.ImplVarName()).To(Equal("initImpl")) + }) + + It("should generate correct export function name", func() { + e := Export{Name: "GetArtistBiography", ExportName: "nd_get_artist_biography"} + Expect(e.ExportFuncName()).To(Equal("_NdGetArtistBiography")) + }) + }) }) diff --git a/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl b/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl new file mode 100644 index 000000000..db87f4769 --- /dev/null +++ b/plugins/cmd/ndpgen/internal/templates/capability.go.tmpl @@ -0,0 +1,150 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the {{.Capability.Interface}} capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package {{.Package}} + +import ( + pdk "github.com/extism/go-pdk" +) + +{{- /* Generate type alias definitions */ -}} +{{- range .Capability.TypeAliases}} + +{{- if .Doc}} +{{formatDoc .Doc}} +{{- end}} +type {{.Name}} {{.Type}} +{{- end}} + +{{- /* Generate const definitions */ -}} +{{- range .Capability.Consts}} +{{- if .Values}} + +const ( +{{- range .Values}} +{{- if .Doc}} +{{formatDoc .Doc | indent 1}} +{{- end}} + {{.Name}} = {{.Value}} +{{- end}} +) +{{- end}} +{{- end}} + +{{- /* Generate struct definitions */ -}} +{{- range .Capability.Structs}} + +// {{.Name}} represents the {{.Name}} data structure. +{{- if .Doc}} +{{formatDoc .Doc}} +{{- end}} +type {{.Name}} struct { +{{- range .Fields}} +{{- if .Doc}} +{{formatDoc .Doc | indent 1}} +{{- end}} + {{.Name}} {{.Type}} `json:"{{.JSONTag}}{{if .OmitEmpty}},omitempty{{end}}"` +{{- end}} +} +{{- end}} + +{{- /* Generate main interface based on required flag */ -}} +{{if .Capability.Required}} + +// {{agentName .Capability}} requires all methods to be implemented. +{{- if .Capability.Doc}} +{{formatDoc .Capability.Doc}} +{{- end}} +type {{agentName .Capability}} interface { +{{- range .Capability.Methods}} + // {{.Name}}{{if .Doc}} - {{.Doc}}{{end}} + {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) +{{- end}} +} +{{- else}} + +// {{agentName .Capability}} is the marker interface for {{.Package}} plugins. +// Implement one or more of the provider interfaces below. +{{- if .Capability.Doc}} +{{formatDoc .Capability.Doc}} +{{- end}} +type {{agentName .Capability}} interface{} +{{- end}} + +{{- /* Generate optional provider interfaces for non-required capabilities */ -}} +{{- if not .Capability.Required}} +{{- range .Capability.Methods}} + +// {{providerInterface .}} provides the {{.Name}} function. +type {{providerInterface .}} interface { + {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) +} +{{- end}} +{{- end}} + +{{- /* Generate implementation function holders */ -}} + +// Internal implementation holders +var ( +{{- range .Capability.Methods}} + {{implVar .}} func({{.Input.Type}}) ({{.Output.Type}}, error) +{{- end}} +) + +// Register registers a {{.Package}} implementation. +{{- if .Capability.Required}} +// All methods are required. +func Register(impl {{agentName .Capability}}) { +{{- range .Capability.Methods}} + {{implVar .}} = impl.{{.Name}} +{{- end}} +} +{{- else}} +// The implementation is checked for optional provider interfaces. +func Register(impl {{agentName .Capability}}) { +{{- range .Capability.Methods}} + if p, ok := impl.({{providerInterface .}}); ok { + {{implVar .}} = p.{{.Name}} + } +{{- end}} +} +{{- end}} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +{{- /* Generate export wrappers */ -}} +{{range .Capability.Methods}} + +//export {{.ExportName}} +func {{exportFunc .}}() int32 { + if {{implVar .}} == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input {{.Input.Type}} + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := {{implVar .}}(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} +{{- end}} diff --git a/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl b/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl new file mode 100644 index 000000000..e2e8ce8ad --- /dev/null +++ b/plugins/cmd/ndpgen/internal/templates/capability_stub.go.tmpl @@ -0,0 +1,95 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package {{.Package}} + +{{- /* Generate type alias definitions */ -}} +{{- range .Capability.TypeAliases}} + +{{- if .Doc}} +{{formatDoc .Doc}} +{{- end}} +type {{.Name}} {{.Type}} +{{- end}} + +{{- /* Generate const definitions */ -}} +{{- range .Capability.Consts}} +{{- if .Values}} + +const ( +{{- range .Values}} +{{- if .Doc}} +{{formatDoc .Doc | indent 1}} +{{- end}} + {{.Name}} = {{.Value}} +{{- end}} +) +{{- end}} +{{- end}} + +{{- /* Generate struct definitions */ -}} +{{- range .Capability.Structs}} + +// {{.Name}} represents the {{.Name}} data structure. +{{- if .Doc}} +{{formatDoc .Doc}} +{{- end}} +type {{.Name}} struct { +{{- range .Fields}} +{{- if .Doc}} +{{formatDoc .Doc | indent 1}} +{{- end}} + {{.Name}} {{.Type}} `json:"{{.JSONTag}}{{if .OmitEmpty}},omitempty{{end}}"` +{{- end}} +} +{{- end}} + +{{- /* Generate main interface based on required flag */ -}} +{{if .Capability.Required}} + +// {{agentName .Capability}} requires all methods to be implemented. +{{- if .Capability.Doc}} +{{formatDoc .Capability.Doc}} +{{- end}} +type {{agentName .Capability}} interface { +{{- range .Capability.Methods}} + // {{.Name}}{{if .Doc}} - {{.Doc}}{{end}} + {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) +{{- end}} +} +{{- else}} + +// {{agentName .Capability}} is the marker interface for {{.Package}} plugins. +// Implement one or more of the provider interfaces below. +{{- if .Capability.Doc}} +{{formatDoc .Capability.Doc}} +{{- end}} +type {{agentName .Capability}} interface{} +{{- end}} + +{{- /* Generate optional provider interfaces for non-required capabilities */ -}} +{{- if not .Capability.Required}} +{{- range .Capability.Methods}} + +// {{providerInterface .}} provides the {{.Name}} function. +type {{providerInterface .}} interface { + {{.Name}}({{.Input.Type}}) ({{.Output.Type}}, error) +} +{{- end}} +{{- end}} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +{{- if .Capability.Required}} +func Register(_ {{agentName .Capability}}) {} +{{- else}} +func Register(_ {{agentName .Capability}}) {} +{{- end}} diff --git a/plugins/cmd/ndpgen/internal/types.go b/plugins/cmd/ndpgen/internal/types.go index af5ccfb6b..ddf5e4acb 100644 --- a/plugins/cmd/ndpgen/internal/types.go +++ b/plugins/cmd/ndpgen/internal/types.go @@ -15,6 +15,103 @@ type Service struct { Structs []StructDef // Structs used by this service } +// Capability represents a parsed capability interface for plugin exports. +type Capability struct { + Name string // Package name from annotation (e.g., "metadata") + Interface string // Go interface name (e.g., "MetadataAgent") + Required bool // If true, all methods must be implemented + Methods []Export // Methods marked with //nd:export + Doc string // Documentation comment for the capability + Structs []StructDef // Structs used by this capability + TypeAliases []TypeAlias // Type aliases used by this capability + Consts []ConstGroup // Const groups used by this capability +} + +// TypeAlias represents a type alias definition (e.g., type ScrobblerErrorType string). +type TypeAlias struct { + Name string // Type name + Type string // Underlying type + Doc string // Documentation comment +} + +// ConstGroup represents a group of const definitions. +type ConstGroup struct { + Type string // Type name for typed consts (empty for untyped) + Values []ConstDef // Const definitions +} + +// ConstDef represents a single const definition. +type ConstDef struct { + Name string // Const name + Value string // Const value + Doc string // Documentation comment +} + +// KnownStructs returns a map of struct names defined in this capability. +func (c Capability) KnownStructs() map[string]bool { + result := make(map[string]bool) + for _, st := range c.Structs { + result[st.Name] = true + } + return result +} + +// Export represents an exported WASM function within a capability. +type Export struct { + Name string // Go method name (e.g., "GetArtistBiography") + ExportName string // WASM export name (e.g., "nd_get_artist_biography") + Input Param // Single input parameter (the struct type) + Output Param // Single output return value (the struct type) + Doc string // Documentation comment for the method +} + +// ProviderInterfaceName returns the optional provider interface name. +// For a method "GetArtistBiography", returns "ArtistBiographyProvider". +func (e Export) ProviderInterfaceName() string { + // Remove "Get", "On", etc. prefixes and add "Provider" suffix + name := e.Name + for _, prefix := range []string{"Get", "On"} { + if strings.HasPrefix(name, prefix) { + name = name[len(prefix):] + break + } + } + return name + "Provider" +} + +// ImplVarName returns the internal implementation variable name. +// For "GetArtistBiography", returns "artistBiographyImpl". +func (e Export) ImplVarName() string { + name := e.Name + for _, prefix := range []string{"Get", "On"} { + if strings.HasPrefix(name, prefix) { + name = name[len(prefix):] + break + } + } + // Convert to camelCase + if len(name) > 0 { + name = strings.ToLower(string(name[0])) + name[1:] + } + return name + "Impl" +} + +// ExportFuncName returns the unexported WASM export function name. +// For "nd_get_artist_biography", returns "_ndGetArtistBiography". +func (e Export) ExportFuncName() string { + // Convert snake_case to PascalCase + parts := strings.Split(e.ExportName, "_") + var result strings.Builder + result.WriteString("_") + for _, part := range parts { + if len(part) > 0 { + result.WriteString(strings.ToUpper(string(part[0]))) + result.WriteString(part[1:]) + } + } + return result.String() +} + // StructDef represents a Go struct type definition. type StructDef struct { Name string // Go struct name (e.g., "Library") diff --git a/plugins/cmd/ndpgen/main.go b/plugins/cmd/ndpgen/main.go index ee1e2492a..2908f10c5 100644 --- a/plugins/cmd/ndpgen/main.go +++ b/plugins/cmd/ndpgen/main.go @@ -1,28 +1,32 @@ // ndpgen generates Navidrome Plugin Development Kit (PDK) code from annotated Go interfaces. // -// This is the unified code generator that replaces hostgen and handles both host function -// wrappers and capability wrappers (when implemented). +// This is the unified code generator that handles both host function wrappers +// and capability export wrappers. // // Usage: // -// ndpgen -input=./plugins/host -output=./plugins/pdk +// # Generate host wrappers (from plugins/host to plugins/pdk) +// ndpgen -host-only -input=./plugins/host -output=./plugins/pdk // -// This generates code into language-specific subdirectories: -// - Go: $output/go/host/ -// - Python: $output/python/host/ -// - Rust: $output/rust/host/ +// # Generate capability wrappers (from plugins/capabilities to plugins/pdk) +// ndpgen -capability-only -input=./plugins/capabilities -output=./plugins/pdk +// +// Output directories: +// - Host functions: $output/go/host/, $output/python/host/, $output/rust/host/ +// - Capabilities: $output/go// (e.g., $output/go/metadata/) // // Flags: // -// -input Input directory containing Go source files with annotated interfaces -// -output Output directory base for generated files (default: same as input) -// -package Output package name for Go (default: host) -// -host-only Generate only host function wrappers (default: true, capability support TBD) -// -go Generate Go client wrappers (default: true when not using -python/-rust) -// -python Generate Python client wrappers (default: false) -// -rust Generate Rust client wrappers (default: false) -// -v Verbose output -// -dry-run Preview generated code without writing files +// -input Input directory containing Go source files with annotated interfaces +// -output Output directory base for generated files (default: same as input) +// -package Output package name for Go (default: host for host-only, auto for capabilities) +// -host-only Generate only host function wrappers +// -capability-only Generate only capability export wrappers +// -go Generate Go client wrappers (default: true when not using -python/-rust) +// -python Generate Python client wrappers (default: false) +// -rust Generate Rust client wrappers (default: false) +// -v Verbose output +// -dry-run Preview generated code without writing files package main import ( @@ -40,11 +44,12 @@ import ( type config struct { inputDir string outputDir string // Base output directory (e.g., plugins/pdk) - goOutputDir string // Go output: $outputDir/go/host + goOutputDir string // Go output: $outputDir/go/host (for host-only) pythonOutputDir string // Python output: $outputDir/python/host rustOutputDir string // Rust output: $outputDir/rust/host pkgName string hostOnly bool + capabilityOnly bool generateGoClient bool generatePyClient bool generateRsClient bool @@ -59,6 +64,15 @@ func main() { os.Exit(1) } + if cfg.capabilityOnly { + if err := runCapabilityGeneration(cfg); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + return + } + + // Default: host-only mode services, err := parseServices(cfg) if err != nil { fmt.Fprintf(os.Stderr, "Error: %v\n", err) @@ -74,25 +88,60 @@ func main() { } } +// runCapabilityGeneration handles capability-only code generation. +func runCapabilityGeneration(cfg *config) error { + capabilities, err := parseCapabilities(cfg) + if err != nil { + return err + } + if len(capabilities) == 0 { + if cfg.verbose { + fmt.Println("No capabilities found") + } + return nil + } + + return generateCapabilityCode(cfg, capabilities) +} + // parseConfig parses command-line flags and returns the configuration. func parseConfig() (*config, error) { var ( - inputDir = flag.String("input", ".", "Input directory containing Go source files") - outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)") - pkgName = flag.String("package", "host", "Output package name for Go (default: host)") - hostOnly = flag.Bool("host-only", true, "Generate only host function wrappers (capability support TBD)") - goClient = flag.Bool("go", false, "Generate Go client wrappers") - pyClient = flag.Bool("python", false, "Generate Python client wrappers") - rsClient = flag.Bool("rust", false, "Generate Rust client wrappers") - verbose = flag.Bool("v", false, "Verbose output") - dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files") + inputDir = flag.String("input", ".", "Input directory containing Go source files") + outputDir = flag.String("output", "", "Base output directory for generated files (default: same as input)") + pkgName = flag.String("package", "", "Output package name for Go (default: host for host-only, auto for capabilities)") + hostOnly = flag.Bool("host-only", false, "Generate only host function wrappers") + capabilityOnly = flag.Bool("capability-only", false, "Generate only capability export wrappers") + goClient = flag.Bool("go", false, "Generate Go client wrappers") + pyClient = flag.Bool("python", false, "Generate Python client wrappers") + rsClient = flag.Bool("rust", false, "Generate Rust client wrappers") + verbose = flag.Bool("v", false, "Verbose output") + dryRun = flag.Bool("dry-run", false, "Preview generated code without writing files") ) flag.Parse() + // Default to host-only if neither mode is specified + if !*hostOnly && !*capabilityOnly { + *hostOnly = true + } + + // Cannot specify both modes + if *hostOnly && *capabilityOnly { + return nil, fmt.Errorf("cannot specify both -host-only and -capability-only") + } + if *outputDir == "" { *outputDir = *inputDir } + // Default package name based on mode + if *pkgName == "" { + if *hostOnly { + *pkgName = "host" + } + // For capability-only, package name is derived from capability annotation + } + absInput, err := filepath.Abs(*inputDir) if err != nil { return nil, fmt.Errorf("resolving input path: %w", err) @@ -119,6 +168,7 @@ func parseConfig() (*config, error) { rustOutputDir: absRustOutput, pkgName: *pkgName, hostOnly: *hostOnly, + capabilityOnly: *capabilityOnly, generateGoClient: *goClient || !anyLangFlag, generatePyClient: *pyClient, generateRsClient: *rsClient, @@ -170,6 +220,109 @@ func parseServices(cfg *config) ([]internal.Service, error) { return services, nil } +// parseCapabilities parses source files and returns discovered capabilities. +func parseCapabilities(cfg *config) ([]internal.Capability, error) { + if cfg.verbose { + fmt.Printf("Input directory: %s\n", cfg.inputDir) + fmt.Printf("Base output directory: %s\n", cfg.outputDir) + fmt.Printf("Capability-only mode: %v\n", cfg.capabilityOnly) + } + + capabilities, err := internal.ParseCapabilities(cfg.inputDir) + if err != nil { + return nil, fmt.Errorf("parsing capability files: %w", err) + } + + if len(capabilities) == 0 { + return nil, nil + } + + if cfg.verbose { + fmt.Printf("Found %d capability(ies)\n", len(capabilities)) + for _, cap := range capabilities { + fmt.Printf(" - %s (%d exports, required=%v)\n", cap.Name, len(cap.Methods), cap.Required) + } + } + + return capabilities, nil +} + +// generateCapabilityCode generates Go export wrappers for all capabilities. +func generateCapabilityCode(cfg *config, capabilities []internal.Capability) error { + for _, cap := range capabilities { + // Output directory is $output/go// + outputDir := filepath.Join(cfg.outputDir, "go", cap.Name) + + if err := generateCapabilityGoCode(cap, outputDir, cfg.dryRun, cfg.verbose); err != nil { + return fmt.Errorf("generating capability code for %s: %w", cap.Name, err) + } + } + + return nil +} + +// generateCapabilityGoCode generates Go export wrapper code for a capability. +func generateCapabilityGoCode(cap internal.Capability, outputDir string, dryRun, verbose bool) error { + // Use the capability name as the package name + pkgName := cap.Name + + // Generate the main WASM code + code, err := internal.GenerateCapabilityGo(cap, pkgName) + if err != nil { + return fmt.Errorf("generating code: %w", err) + } + + formatted, err := format.Source(code) + if err != nil { + return fmt.Errorf("formatting code: %w\nRaw code:\n%s", err, code) + } + + mainFile := filepath.Join(outputDir, cap.Name+".go") + + if dryRun { + fmt.Printf("=== %s ===\n%s\n", mainFile, formatted) + } else { + if err := os.MkdirAll(outputDir, 0755); err != nil { + return fmt.Errorf("creating output directory: %w", err) + } + + if err := os.WriteFile(mainFile, formatted, 0600); err != nil { + return fmt.Errorf("writing file: %w", err) + } + + if verbose { + fmt.Printf("Generated capability code: %s\n", mainFile) + } + } + + // Generate the stub code for non-WASM platforms + stubCode, err := internal.GenerateCapabilityGoStub(cap, pkgName) + if err != nil { + return fmt.Errorf("generating stub code: %w", err) + } + + formattedStub, err := format.Source(stubCode) + if err != nil { + return fmt.Errorf("formatting stub code: %w\nRaw code:\n%s", err, stubCode) + } + + stubFile := filepath.Join(outputDir, cap.Name+"_stub.go") + + if dryRun { + fmt.Printf("=== %s ===\n%s\n", stubFile, formattedStub) + } else { + if err := os.WriteFile(stubFile, formattedStub, 0600); err != nil { + return fmt.Errorf("writing stub file: %w", err) + } + + if verbose { + fmt.Printf("Generated capability stub: %s\n", stubFile) + } + } + + return nil +} + // generateAllCode generates all requested code for the services. func generateAllCode(cfg *config, services []internal.Service) error { for _, svc := range services { diff --git a/plugins/examples/wikimedia/go.mod b/plugins/examples/wikimedia/go.mod index 15afcd443..651524e4d 100644 --- a/plugins/examples/wikimedia/go.mod +++ b/plugins/examples/wikimedia/go.mod @@ -1,5 +1,10 @@ module wikimedia-plugin -go 1.23 +go 1.25 -require github.com/extism/go-pdk v1.1.3 +require ( + github.com/extism/go-pdk v1.1.3 + github.com/navidrome/navidrome v0.0.0-00010101000000-000000000000 +) + +replace github.com/navidrome/navidrome => ../../.. diff --git a/plugins/examples/wikimedia/main.go b/plugins/examples/wikimedia/main.go index d63c39dea..25b95581a 100644 --- a/plugins/examples/wikimedia/main.go +++ b/plugins/examples/wikimedia/main.go @@ -1,15 +1,7 @@ // Wikimedia plugin for Navidrome - fetches artist metadata from Wikidata, DBpedia and Wikipedia. // -// This plugin was generated using: -// -// xtp plugin init --schema-file plugins/schemas/metadata_agent.yaml --template go --path ./wikimedia --name wikimedia-plugin -// // Build with: // -// xtp plugin build -// -// Or manually: -// // tinygo build -o wikimedia.wasm -target wasip1 -buildmode=c-shared . // // Install by copying the .ndp file to your Navidrome plugins folder. @@ -22,7 +14,23 @@ import ( "net/url" "strings" - "github.com/extism/go-pdk" + pdk "github.com/extism/go-pdk" + "github.com/navidrome/navidrome/plugins/pdk/go/metadata" +) + +// wikimediaPlugin implements the metadata provider interfaces for the methods we support. +type wikimediaPlugin struct{} + +// init registers the plugin implementation +func init() { + metadata.Register(&wikimediaPlugin{}) +} + +// Ensure wikimediaPlugin implements the provider interfaces +var ( + _ metadata.ArtistURLProvider = (*wikimediaPlugin)(nil) + _ metadata.ArtistBiographyProvider = (*wikimediaPlugin)(nil) + _ metadata.ArtistImagesProvider = (*wikimediaPlugin)(nil) ) const ( @@ -232,15 +240,15 @@ func getMBID(mbid *string) string { return *mbid } -// NdGetArtistUrl returns the Wikipedia URL for an artist -func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) { - mbid := getMBID(input.Mbid) +// GetArtistURL returns the Wikipedia URL for an artist +func (*wikimediaPlugin) GetArtistURL(input metadata.ArtistInput) (metadata.ArtistURLOutput, error) { + mbid := getMBID(input.MBID) pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistURL: name=%s, mbid=%s", input.Name, mbid)) // 1. Try Wikidata (MBID first, then name) wikiURL, err := getWikidataWikipediaURL(mbid, input.Name) if err == nil && wikiURL != "" { - return ArtistURLOutput{Url: wikiURL}, nil + return metadata.ArtistURLOutput{URL: wikiURL}, nil } if err != nil { pdk.Log(pdk.LogDebug, fmt.Sprintf("Wikidata URL failed: %v", err)) @@ -250,7 +258,7 @@ func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) { if input.Name != "" { wikiURL, err = getDBpediaWikipediaURL(input.Name) if err == nil && wikiURL != "" { - return ArtistURLOutput{Url: wikiURL}, nil + return metadata.ArtistURLOutput{URL: wikiURL}, nil } if err != nil { pdk.Log(pdk.LogDebug, fmt.Sprintf("DBpedia URL failed: %v", err)) @@ -261,15 +269,15 @@ func NdGetArtistUrl(input ArtistInput) (ArtistURLOutput, error) { if input.Name != "" { searchURL := fmt.Sprintf("https://en.wikipedia.org/w/index.php?search=%s", url.QueryEscape(input.Name)) pdk.Log(pdk.LogInfo, fmt.Sprintf("URL not found, falling back to search URL: %s", searchURL)) - return ArtistURLOutput{Url: searchURL}, nil + return metadata.ArtistURLOutput{URL: searchURL}, nil } - return ArtistURLOutput{}, errors.New("could not determine Wikipedia URL") + return metadata.ArtistURLOutput{}, errors.New("could not determine Wikipedia URL") } -// NdGetArtistBiography returns the biography for an artist from Wikipedia -func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) { - mbid := getMBID(input.Mbid) +// GetArtistBiography returns the biography for an artist from Wikipedia +func (*wikimediaPlugin) GetArtistBiography(input metadata.ArtistInput) (metadata.ArtistBiographyOutput, error) { + mbid := getMBID(input.MBID) pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistBiography: name=%s, mbid=%s", input.Name, mbid)) // 1. Get Wikipedia URL (using the logic from GetArtistURL) @@ -297,7 +305,7 @@ func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) { bio, err := getWikipediaExtract(pageTitle) if err == nil && bio != "" { pdk.Log(pdk.LogDebug, "Found Wikipedia extract") - return ArtistBiographyOutput{Biography: bio}, nil + return metadata.ArtistBiographyOutput{Biography: bio}, nil } pdk.Log(pdk.LogDebug, fmt.Sprintf("Wikipedia extract failed: %v", err)) } else { @@ -311,18 +319,18 @@ func NdGetArtistBiography(input ArtistInput) (ArtistBiographyOutput, error) { bio, err := getDBpediaComment(input.Name) if err == nil && bio != "" { pdk.Log(pdk.LogDebug, "Found DBpedia comment") - return ArtistBiographyOutput{Biography: bio}, nil + return metadata.ArtistBiographyOutput{Biography: bio}, nil } pdk.Log(pdk.LogDebug, fmt.Sprintf("DBpedia comment failed: %v", err)) } pdk.Log(pdk.LogInfo, fmt.Sprintf("Biography not found for: %s (%s)", input.Name, mbid)) - return ArtistBiographyOutput{}, errors.New("biography not found") + return metadata.ArtistBiographyOutput{}, errors.New("biography not found") } -// NdGetArtistImages returns artist images from Wikidata -func NdGetArtistImages(input ArtistInput) (ArtistImagesOutput, error) { - mbid := getMBID(input.Mbid) +// GetArtistImages returns artist images from Wikidata +func (*wikimediaPlugin) GetArtistImages(input metadata.ArtistInput) (metadata.ArtistImagesOutput, error) { + mbid := getMBID(input.MBID) pdk.Log(pdk.LogDebug, fmt.Sprintf("GetArtistImages: name=%s, mbid=%s", input.Name, mbid)) var q string @@ -332,43 +340,23 @@ func NdGetArtistImages(input ArtistInput) (ArtistImagesOutput, error) { escapedName := strings.ReplaceAll(input.Name, "\"", "\\\"") q = fmt.Sprintf(`SELECT ?img WHERE { ?artist rdfs:label "%s"@en; wdt:P18 ?img } LIMIT 1`, escapedName) } else { - return ArtistImagesOutput{}, errors.New("MBID or Name required for Wikidata Image lookup") + return metadata.ArtistImagesOutput{}, errors.New("MBID or Name required for Wikidata Image lookup") } result, err := sparqlQuery(wikidataEndpoint, q) if err != nil { pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, mbid)) - return ArtistImagesOutput{}, errors.New("image not found") + return metadata.ArtistImagesOutput{}, errors.New("image not found") } if result.Results.Bindings[0].Img != nil { - return ArtistImagesOutput{ - Images: []ImageInfo{{Url: result.Results.Bindings[0].Img.Value, Size: 0}}, + return metadata.ArtistImagesOutput{ + Images: []metadata.ImageInfo{{URL: result.Results.Bindings[0].Img.Value, Size: 0}}, }, nil } pdk.Log(pdk.LogInfo, fmt.Sprintf("Image not found for: %s (%s)", input.Name, mbid)) - return ArtistImagesOutput{}, errors.New("image not found") + return metadata.ArtistImagesOutput{}, errors.New("image not found") } -// The functions below are not implemented - they return errors to indicate -// Navidrome should fall back to other agents. - -func NdGetAlbumImages(input AlbumInput) (AlbumImagesOutput, error) { - return AlbumImagesOutput{}, errors.New("not implemented") -} - -func NdGetAlbumInfo(input AlbumInput) (AlbumInfoOutput, error) { - return AlbumInfoOutput{}, errors.New("not implemented") -} - -func NdGetArtistMbid(input ArtistMBIDInput) (ArtistMBIDOutput, error) { - return ArtistMBIDOutput{}, errors.New("not implemented") -} - -func NdGetArtistTopSongs(input TopSongsInput) (TopSongsOutput, error) { - return TopSongsOutput{}, errors.New("not implemented") -} - -func NdGetSimilarArtists(input SimilarArtistsInput) (SimilarArtistsOutput, error) { - return SimilarArtistsOutput{}, errors.New("not implemented") -} +// Required main function - init() handles registration +func main() {} diff --git a/plugins/examples/wikimedia/pdk.gen.go b/plugins/examples/wikimedia/pdk.gen.go deleted file mode 100755 index 029063d7e..000000000 --- a/plugins/examples/wikimedia/pdk.gen.go +++ /dev/null @@ -1,376 +0,0 @@ -// THIS FILE WAS GENERATED BY `xtp-go-bindgen`. DO NOT EDIT. -package main - -import ( - pdk "github.com/extism/go-pdk" -) - -//export nd_get_album_images -func _NdGetAlbumImages() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetAlbumImages: getting JSON input") - var input AlbumInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetAlbumImages: calling implementation function") - output, err := NdGetAlbumImages(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetAlbumImages: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetAlbumImages: returning") - return 0 -} - -//export nd_get_album_info -func _NdGetAlbumInfo() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: getting JSON input") - var input AlbumInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: calling implementation function") - output, err := NdGetAlbumInfo(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetAlbumInfo: returning") - return 0 -} - -//export nd_get_artist_biography -func _NdGetArtistBiography() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetArtistBiography: getting JSON input") - var input ArtistInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistBiography: calling implementation function") - output, err := NdGetArtistBiography(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistBiography: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistBiography: returning") - return 0 -} - -//export nd_get_artist_images -func _NdGetArtistImages() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetArtistImages: getting JSON input") - var input ArtistInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistImages: calling implementation function") - output, err := NdGetArtistImages(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistImages: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistImages: returning") - return 0 -} - -//export nd_get_artist_mbid -func _NdGetArtistMbid() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetArtistMbid: getting JSON input") - var input ArtistMBIDInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistMbid: calling implementation function") - output, err := NdGetArtistMbid(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistMbid: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistMbid: returning") - return 0 -} - -//export nd_get_artist_top_songs -func _NdGetArtistTopSongs() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: getting JSON input") - var input TopSongsInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: calling implementation function") - output, err := NdGetArtistTopSongs(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistTopSongs: returning") - return 0 -} - -//export nd_get_artist_url -func _NdGetArtistUrl() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetArtistUrl: getting JSON input") - var input ArtistInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistUrl: calling implementation function") - output, err := NdGetArtistUrl(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistUrl: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetArtistUrl: returning") - return 0 -} - -//export nd_get_similar_artists -func _NdGetSimilarArtists() int32 { - var err error - _ = err - pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: getting JSON input") - var input SimilarArtistsInput - err = pdk.InputJSON(&input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: calling implementation function") - output, err := NdGetSimilarArtists(input) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: setting JSON output") - err = pdk.OutputJSON(output) - if err != nil { - pdk.SetError(err) - return -1 - } - - pdk.Log(pdk.LogDebug, "NdGetSimilarArtists: returning") - return 0 -} - -// Output for GetAlbumImages -type AlbumImagesOutput struct { - // List of album images - Images []ImageInfo `json:"images"` -} - -// Output for GetAlbumInfo -type AlbumInfoOutput struct { - // The album description/notes - Description string `json:"description"` - // The MusicBrainz ID for the album - Mbid string `json:"mbid"` - // The album name - Name string `json:"name"` - // The external URL for the album - Url string `json:"url"` -} - -// Common input for album-related functions -type AlbumInput struct { - // The album artist name - Artist string `json:"artist"` - // The MusicBrainz ID for the album (if known) - Mbid *string `json:"mbid,omitempty"` - // The album name - Name string `json:"name"` -} - -// Output for GetArtistBiography -type ArtistBiographyOutput struct { - // The artist biography text - Biography string `json:"biography"` -} - -// Output for GetArtistImages -type ArtistImagesOutput struct { - // List of artist images - Images []ImageInfo `json:"images"` -} - -// Common input for artist-related functions -type ArtistInput struct { - // The internal Navidrome artist ID - Id string `json:"id"` - // The MusicBrainz ID for the artist (if known) - Mbid *string `json:"mbid,omitempty"` - // The artist name - Name string `json:"name"` -} - -// Input for GetArtistMBID -type ArtistMBIDInput struct { - // The internal Navidrome artist ID - Id string `json:"id"` - // The artist name - Name string `json:"name"` -} - -// Output for GetArtistMBID -type ArtistMBIDOutput struct { - // The MusicBrainz ID for the artist - Mbid string `json:"mbid"` -} - -// Reference to an artist with name and optional MBID -type ArtistRef struct { - // The MusicBrainz ID for the artist - Mbid *string `json:"mbid,omitempty"` - // The artist name - Name string `json:"name"` -} - -// Output for GetArtistURL -type ArtistURLOutput struct { - // The external URL for the artist - Url string `json:"url"` -} - -// Image with URL and size -type ImageInfo struct { - // The size of the image in pixels (width or height) - Size int32 `json:"size"` - // The URL of the image - Url string `json:"url"` -} - -// Input for GetSimilarArtists -type SimilarArtistsInput struct { - // The internal Navidrome artist ID - Id string `json:"id"` - // Maximum number of similar artists to return - Limit int32 `json:"limit"` - // The MusicBrainz ID for the artist (if known) - Mbid *string `json:"mbid,omitempty"` - // The artist name - Name string `json:"name"` -} - -// Output for GetSimilarArtists -type SimilarArtistsOutput struct { - // List of similar artists - Artists []ArtistRef `json:"artists"` -} - -// Reference to a song with name and optional MBID -type SongRef struct { - // The MusicBrainz ID for the song - Mbid *string `json:"mbid,omitempty"` - // The song name - Name string `json:"name"` -} - -// Input for GetArtistTopSongs -type TopSongsInput struct { - // Maximum number of top songs to return - Count int32 `json:"count"` - // The internal Navidrome artist ID - Id string `json:"id"` - // The MusicBrainz ID for the artist (if known) - Mbid *string `json:"mbid,omitempty"` - // The artist name - Name string `json:"name"` -} - -// Output for GetArtistTopSongs -type TopSongsOutput struct { - // List of top songs - Songs []SongRef `json:"songs"` -} diff --git a/plugins/pdk/go/lifecycle/lifecycle.go b/plugins/pdk/go/lifecycle/lifecycle.go new file mode 100644 index 000000000..b8b395ad2 --- /dev/null +++ b/plugins/pdk/go/lifecycle/lifecycle.go @@ -0,0 +1,86 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the Lifecycle capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package lifecycle + +import ( + pdk "github.com/extism/go-pdk" +) + +// OnInitInput represents the OnInitInput data structure. +// OnInitInput is the input provided to the init callback. +// Currently empty, reserved for future use. +type OnInitInput struct { +} + +// OnInitOutput represents the OnInitOutput data structure. +// OnInitOutput is the output from the init callback. +type OnInitOutput struct { + // Error is the error message if initialization failed. + // Empty or null indicates success. + // The error is logged but does not prevent the plugin from being loaded. + Error *string `json:"error,omitempty"` +} + +// Lifecycle is the marker interface for lifecycle plugins. +// Implement one or more of the provider interfaces below. +// Lifecycle provides plugin lifecycle hooks. +// This capability allows plugins to perform initialization when loaded, +// such as establishing connections, starting background processes, or +// validating configuration. +// +// The OnInit function is called once when the plugin is loaded, and is NOT +// called when the plugin is hot-reloaded. Plugins should not assume this +// function will be called on every startup. +type Lifecycle interface{} + +// InitProvider provides the OnInit function. +type InitProvider interface { + OnInit(OnInitInput) (OnInitOutput, error) +} // Internal implementation holders +var ( + initImpl func(OnInitInput) (OnInitOutput, error) +) + +// Register registers a lifecycle implementation. +// The implementation is checked for optional provider interfaces. +func Register(impl Lifecycle) { + if p, ok := impl.(InitProvider); ok { + initImpl = p.OnInit + } +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//export nd_on_init +func _NdOnInit() int32 { + if initImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input OnInitInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := initImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/lifecycle/lifecycle_stub.go b/plugins/pdk/go/lifecycle/lifecycle_stub.go new file mode 100644 index 000000000..e0c6a2eda --- /dev/null +++ b/plugins/pdk/go/lifecycle/lifecycle_stub.go @@ -0,0 +1,48 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package lifecycle + +// OnInitInput represents the OnInitInput data structure. +// OnInitInput is the input provided to the init callback. +// Currently empty, reserved for future use. +type OnInitInput struct { +} + +// OnInitOutput represents the OnInitOutput data structure. +// OnInitOutput is the output from the init callback. +type OnInitOutput struct { + // Error is the error message if initialization failed. + // Empty or null indicates success. + // The error is logged but does not prevent the plugin from being loaded. + Error *string `json:"error,omitempty"` +} + +// Lifecycle is the marker interface for lifecycle plugins. +// Implement one or more of the provider interfaces below. +// Lifecycle provides plugin lifecycle hooks. +// This capability allows plugins to perform initialization when loaded, +// such as establishing connections, starting background processes, or +// validating configuration. +// +// The OnInit function is called once when the plugin is loaded, and is NOT +// called when the plugin is hot-reloaded. Plugins should not assume this +// function will be called on every startup. +type Lifecycle interface{} + +// InitProvider provides the OnInit function. +type InitProvider interface { + OnInit(OnInitInput) (OnInitOutput, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ Lifecycle) {} diff --git a/plugins/pdk/go/metadata/metadata.go b/plugins/pdk/go/metadata/metadata.go new file mode 100644 index 000000000..30fde3d98 --- /dev/null +++ b/plugins/pdk/go/metadata/metadata.go @@ -0,0 +1,467 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the MetadataAgent capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package metadata + +import ( + pdk "github.com/extism/go-pdk" +) + +// ArtistMBIDInput represents the ArtistMBIDInput data structure. +// ArtistMBIDInput is the input for GetArtistMBID. +type ArtistMBIDInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` +} + +// TopSongsInput represents the TopSongsInput data structure. +// TopSongsInput is the input for GetArtistTopSongs. +type TopSongsInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` + // Count is the maximum number of top songs to return. + Count int32 `json:"count"` +} + +// SimilarArtistsInput represents the SimilarArtistsInput data structure. +// SimilarArtistsInput is the input for GetSimilarArtists. +type SimilarArtistsInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` + // Limit is the maximum number of similar artists to return. + Limit int32 `json:"limit"` +} + +// TopSongsOutput represents the TopSongsOutput data structure. +// TopSongsOutput is the output for GetArtistTopSongs. +type TopSongsOutput struct { + // Songs is the list of top songs. + Songs []SongRef `json:"songs"` +} + +// AlbumInput represents the AlbumInput data structure. +// AlbumInput is the common input for album-related functions. +type AlbumInput struct { + // Name is the album name. + Name string `json:"name"` + // Artist is the album artist name. + Artist string `json:"artist"` + // MBID is the MusicBrainz ID for the album (if known). + MBID *string `json:"mbid,omitempty"` +} + +// AlbumInfoOutput represents the AlbumInfoOutput data structure. +// AlbumInfoOutput is the output for GetAlbumInfo. +type AlbumInfoOutput struct { + // Name is the album name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the album. + MBID string `json:"mbid"` + // Description is the album description/notes. + Description string `json:"description"` + // URL is the external URL for the album. + URL string `json:"url"` +} + +// AlbumImagesOutput represents the AlbumImagesOutput data structure. +// AlbumImagesOutput is the output for GetAlbumImages. +type AlbumImagesOutput struct { + // Images is the list of album images. + Images []ImageInfo `json:"images"` +} + +// ArtistRef represents the ArtistRef data structure. +// ArtistRef is a reference to an artist with name and optional MBID. +type ArtistRef struct { + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist. + MBID *string `json:"mbid,omitempty"` +} + +// ArtistURLOutput represents the ArtistURLOutput data structure. +// ArtistURLOutput is the output for GetArtistURL. +type ArtistURLOutput struct { + // URL is the external URL for the artist. + URL string `json:"url"` +} + +// ArtistBiographyOutput represents the ArtistBiographyOutput data structure. +// ArtistBiographyOutput is the output for GetArtistBiography. +type ArtistBiographyOutput struct { + // Biography is the artist biography text. + Biography string `json:"biography"` +} + +// SimilarArtistsOutput represents the SimilarArtistsOutput data structure. +// SimilarArtistsOutput is the output for GetSimilarArtists. +type SimilarArtistsOutput struct { + // Artists is the list of similar artists. + Artists []ArtistRef `json:"artists"` +} + +// ArtistImagesOutput represents the ArtistImagesOutput data structure. +// ArtistImagesOutput is the output for GetArtistImages. +type ArtistImagesOutput struct { + // Images is the list of artist images. + Images []ImageInfo `json:"images"` +} + +// ImageInfo represents the ImageInfo data structure. +// ImageInfo represents an image with URL and size. +type ImageInfo struct { + // URL is the URL of the image. + URL string `json:"url"` + // Size is the size of the image in pixels (width or height). + Size int32 `json:"size"` +} + +// ArtistMBIDOutput represents the ArtistMBIDOutput data structure. +// ArtistMBIDOutput is the output for GetArtistMBID. +type ArtistMBIDOutput struct { + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid"` +} + +// ArtistInput represents the ArtistInput data structure. +// ArtistInput is the common input for artist-related functions. +type ArtistInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` +} + +// SongRef represents the SongRef data structure. +// SongRef is a reference to a song with name and optional MBID. +type SongRef struct { + // Name is the song name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the song. + MBID *string `json:"mbid,omitempty"` +} + +// Metadata is the marker interface for metadata plugins. +// Implement one or more of the provider interfaces below. +// MetadataAgent provides artist and album metadata retrieval. +// This capability allows plugins to provide external metadata for artists and albums, +// such as biographies, images, similar artists, and top songs. +// +// Plugins implementing this capability can choose which methods to implement. +// Each method is optional - plugins only need to provide the functionality they support. +type Metadata interface{} + +// ArtistMBIDProvider provides the GetArtistMBID function. +type ArtistMBIDProvider interface { + GetArtistMBID(ArtistMBIDInput) (ArtistMBIDOutput, error) +} + +// ArtistURLProvider provides the GetArtistURL function. +type ArtistURLProvider interface { + GetArtistURL(ArtistInput) (ArtistURLOutput, error) +} + +// ArtistBiographyProvider provides the GetArtistBiography function. +type ArtistBiographyProvider interface { + GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error) +} + +// SimilarArtistsProvider provides the GetSimilarArtists function. +type SimilarArtistsProvider interface { + GetSimilarArtists(SimilarArtistsInput) (SimilarArtistsOutput, error) +} + +// ArtistImagesProvider provides the GetArtistImages function. +type ArtistImagesProvider interface { + GetArtistImages(ArtistInput) (ArtistImagesOutput, error) +} + +// ArtistTopSongsProvider provides the GetArtistTopSongs function. +type ArtistTopSongsProvider interface { + GetArtistTopSongs(TopSongsInput) (TopSongsOutput, error) +} + +// AlbumInfoProvider provides the GetAlbumInfo function. +type AlbumInfoProvider interface { + GetAlbumInfo(AlbumInput) (AlbumInfoOutput, error) +} + +// AlbumImagesProvider provides the GetAlbumImages function. +type AlbumImagesProvider interface { + GetAlbumImages(AlbumInput) (AlbumImagesOutput, error) +} // Internal implementation holders +var ( + artistMBIDImpl func(ArtistMBIDInput) (ArtistMBIDOutput, error) + artistURLImpl func(ArtistInput) (ArtistURLOutput, error) + artistBiographyImpl func(ArtistInput) (ArtistBiographyOutput, error) + similarArtistsImpl func(SimilarArtistsInput) (SimilarArtistsOutput, error) + artistImagesImpl func(ArtistInput) (ArtistImagesOutput, error) + artistTopSongsImpl func(TopSongsInput) (TopSongsOutput, error) + albumInfoImpl func(AlbumInput) (AlbumInfoOutput, error) + albumImagesImpl func(AlbumInput) (AlbumImagesOutput, error) +) + +// Register registers a metadata implementation. +// The implementation is checked for optional provider interfaces. +func Register(impl Metadata) { + if p, ok := impl.(ArtistMBIDProvider); ok { + artistMBIDImpl = p.GetArtistMBID + } + if p, ok := impl.(ArtistURLProvider); ok { + artistURLImpl = p.GetArtistURL + } + if p, ok := impl.(ArtistBiographyProvider); ok { + artistBiographyImpl = p.GetArtistBiography + } + if p, ok := impl.(SimilarArtistsProvider); ok { + similarArtistsImpl = p.GetSimilarArtists + } + if p, ok := impl.(ArtistImagesProvider); ok { + artistImagesImpl = p.GetArtistImages + } + if p, ok := impl.(ArtistTopSongsProvider); ok { + artistTopSongsImpl = p.GetArtistTopSongs + } + if p, ok := impl.(AlbumInfoProvider); ok { + albumInfoImpl = p.GetAlbumInfo + } + if p, ok := impl.(AlbumImagesProvider); ok { + albumImagesImpl = p.GetAlbumImages + } +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//export nd_get_artist_mbid +func _NdGetArtistMbid() int32 { + if artistMBIDImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input ArtistMBIDInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := artistMBIDImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_artist_url +func _NdGetArtistUrl() int32 { + if artistURLImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input ArtistInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := artistURLImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_artist_biography +func _NdGetArtistBiography() int32 { + if artistBiographyImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input ArtistInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := artistBiographyImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_similar_artists +func _NdGetSimilarArtists() int32 { + if similarArtistsImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input SimilarArtistsInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := similarArtistsImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_artist_images +func _NdGetArtistImages() int32 { + if artistImagesImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input ArtistInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := artistImagesImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_artist_top_songs +func _NdGetArtistTopSongs() int32 { + if artistTopSongsImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input TopSongsInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := artistTopSongsImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_album_info +func _NdGetAlbumInfo() int32 { + if albumInfoImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input AlbumInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := albumInfoImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_get_album_images +func _NdGetAlbumImages() int32 { + if albumImagesImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input AlbumInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := albumImagesImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/metadata/metadata_stub.go b/plugins/pdk/go/metadata/metadata_stub.go new file mode 100644 index 000000000..eb969e0a7 --- /dev/null +++ b/plugins/pdk/go/metadata/metadata_stub.go @@ -0,0 +1,212 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package metadata + +// ArtistMBIDInput represents the ArtistMBIDInput data structure. +// ArtistMBIDInput is the input for GetArtistMBID. +type ArtistMBIDInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` +} + +// TopSongsInput represents the TopSongsInput data structure. +// TopSongsInput is the input for GetArtistTopSongs. +type TopSongsInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` + // Count is the maximum number of top songs to return. + Count int32 `json:"count"` +} + +// SimilarArtistsInput represents the SimilarArtistsInput data structure. +// SimilarArtistsInput is the input for GetSimilarArtists. +type SimilarArtistsInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` + // Limit is the maximum number of similar artists to return. + Limit int32 `json:"limit"` +} + +// TopSongsOutput represents the TopSongsOutput data structure. +// TopSongsOutput is the output for GetArtistTopSongs. +type TopSongsOutput struct { + // Songs is the list of top songs. + Songs []SongRef `json:"songs"` +} + +// AlbumInput represents the AlbumInput data structure. +// AlbumInput is the common input for album-related functions. +type AlbumInput struct { + // Name is the album name. + Name string `json:"name"` + // Artist is the album artist name. + Artist string `json:"artist"` + // MBID is the MusicBrainz ID for the album (if known). + MBID *string `json:"mbid,omitempty"` +} + +// AlbumInfoOutput represents the AlbumInfoOutput data structure. +// AlbumInfoOutput is the output for GetAlbumInfo. +type AlbumInfoOutput struct { + // Name is the album name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the album. + MBID string `json:"mbid"` + // Description is the album description/notes. + Description string `json:"description"` + // URL is the external URL for the album. + URL string `json:"url"` +} + +// AlbumImagesOutput represents the AlbumImagesOutput data structure. +// AlbumImagesOutput is the output for GetAlbumImages. +type AlbumImagesOutput struct { + // Images is the list of album images. + Images []ImageInfo `json:"images"` +} + +// ArtistRef represents the ArtistRef data structure. +// ArtistRef is a reference to an artist with name and optional MBID. +type ArtistRef struct { + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist. + MBID *string `json:"mbid,omitempty"` +} + +// ArtistURLOutput represents the ArtistURLOutput data structure. +// ArtistURLOutput is the output for GetArtistURL. +type ArtistURLOutput struct { + // URL is the external URL for the artist. + URL string `json:"url"` +} + +// ArtistBiographyOutput represents the ArtistBiographyOutput data structure. +// ArtistBiographyOutput is the output for GetArtistBiography. +type ArtistBiographyOutput struct { + // Biography is the artist biography text. + Biography string `json:"biography"` +} + +// SimilarArtistsOutput represents the SimilarArtistsOutput data structure. +// SimilarArtistsOutput is the output for GetSimilarArtists. +type SimilarArtistsOutput struct { + // Artists is the list of similar artists. + Artists []ArtistRef `json:"artists"` +} + +// ArtistImagesOutput represents the ArtistImagesOutput data structure. +// ArtistImagesOutput is the output for GetArtistImages. +type ArtistImagesOutput struct { + // Images is the list of artist images. + Images []ImageInfo `json:"images"` +} + +// ImageInfo represents the ImageInfo data structure. +// ImageInfo represents an image with URL and size. +type ImageInfo struct { + // URL is the URL of the image. + URL string `json:"url"` + // Size is the size of the image in pixels (width or height). + Size int32 `json:"size"` +} + +// ArtistMBIDOutput represents the ArtistMBIDOutput data structure. +// ArtistMBIDOutput is the output for GetArtistMBID. +type ArtistMBIDOutput struct { + // MBID is the MusicBrainz ID for the artist. + MBID string `json:"mbid"` +} + +// ArtistInput represents the ArtistInput data structure. +// ArtistInput is the common input for artist-related functions. +type ArtistInput struct { + // ID is the internal Navidrome artist ID. + ID string `json:"id"` + // Name is the artist name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the artist (if known). + MBID *string `json:"mbid,omitempty"` +} + +// SongRef represents the SongRef data structure. +// SongRef is a reference to a song with name and optional MBID. +type SongRef struct { + // Name is the song name. + Name string `json:"name"` + // MBID is the MusicBrainz ID for the song. + MBID *string `json:"mbid,omitempty"` +} + +// Metadata is the marker interface for metadata plugins. +// Implement one or more of the provider interfaces below. +// MetadataAgent provides artist and album metadata retrieval. +// This capability allows plugins to provide external metadata for artists and albums, +// such as biographies, images, similar artists, and top songs. +// +// Plugins implementing this capability can choose which methods to implement. +// Each method is optional - plugins only need to provide the functionality they support. +type Metadata interface{} + +// ArtistMBIDProvider provides the GetArtistMBID function. +type ArtistMBIDProvider interface { + GetArtistMBID(ArtistMBIDInput) (ArtistMBIDOutput, error) +} + +// ArtistURLProvider provides the GetArtistURL function. +type ArtistURLProvider interface { + GetArtistURL(ArtistInput) (ArtistURLOutput, error) +} + +// ArtistBiographyProvider provides the GetArtistBiography function. +type ArtistBiographyProvider interface { + GetArtistBiography(ArtistInput) (ArtistBiographyOutput, error) +} + +// SimilarArtistsProvider provides the GetSimilarArtists function. +type SimilarArtistsProvider interface { + GetSimilarArtists(SimilarArtistsInput) (SimilarArtistsOutput, error) +} + +// ArtistImagesProvider provides the GetArtistImages function. +type ArtistImagesProvider interface { + GetArtistImages(ArtistInput) (ArtistImagesOutput, error) +} + +// ArtistTopSongsProvider provides the GetArtistTopSongs function. +type ArtistTopSongsProvider interface { + GetArtistTopSongs(TopSongsInput) (TopSongsOutput, error) +} + +// AlbumInfoProvider provides the GetAlbumInfo function. +type AlbumInfoProvider interface { + GetAlbumInfo(AlbumInput) (AlbumInfoOutput, error) +} + +// AlbumImagesProvider provides the GetAlbumImages function. +type AlbumImagesProvider interface { + GetAlbumImages(AlbumInput) (AlbumImagesOutput, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ Metadata) {} diff --git a/plugins/pdk/go/scheduler/scheduler.go b/plugins/pdk/go/scheduler/scheduler.go new file mode 100644 index 000000000..537ae21cb --- /dev/null +++ b/plugins/pdk/go/scheduler/scheduler.go @@ -0,0 +1,90 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the SchedulerCallback capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package scheduler + +import ( + pdk "github.com/extism/go-pdk" +) + +// SchedulerCallbackOutput represents the SchedulerCallbackOutput data structure. +// SchedulerCallbackOutput is the output from the scheduler callback. +type SchedulerCallbackOutput struct { + // Error is the error message if the callback failed to process the scheduled task. + // Empty or null indicates success. The error is logged but does not + // affect the scheduling system. + Error *string `json:"error,omitempty"` +} + +// SchedulerCallbackInput represents the SchedulerCallbackInput data structure. +// SchedulerCallbackInput is the input provided when a scheduled task fires. +type SchedulerCallbackInput struct { + // ScheduleID is the unique identifier for this scheduled task. + // This is either the ID provided when scheduling, or an auto-generated UUID if none was specified. + ScheduleID string `json:"scheduleId"` + // Payload is the payload data that was provided when the task was scheduled. + // Can be used to pass context or parameters to the callback handler. + Payload string `json:"payload"` + // IsRecurring is true if this is a recurring schedule (created via ScheduleRecurring), + // false if it's a one-time schedule (created via ScheduleOneTime). + IsRecurring bool `json:"isRecurring"` +} + +// Scheduler is the marker interface for scheduler plugins. +// Implement one or more of the provider interfaces below. +// SchedulerCallback provides scheduled task handling. +// This capability allows plugins to receive callbacks when their scheduled tasks execute. +// Plugins that use the scheduler host service must implement this capability +// to handle task execution. +type Scheduler interface{} + +// SchedulerCallbackProvider provides the OnSchedulerCallback function. +type SchedulerCallbackProvider interface { + OnSchedulerCallback(SchedulerCallbackInput) (SchedulerCallbackOutput, error) +} // Internal implementation holders +var ( + schedulerCallbackImpl func(SchedulerCallbackInput) (SchedulerCallbackOutput, error) +) + +// Register registers a scheduler implementation. +// The implementation is checked for optional provider interfaces. +func Register(impl Scheduler) { + if p, ok := impl.(SchedulerCallbackProvider); ok { + schedulerCallbackImpl = p.OnSchedulerCallback + } +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//export nd_scheduler_callback +func _NdSchedulerCallback() int32 { + if schedulerCallbackImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input SchedulerCallbackInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := schedulerCallbackImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/scheduler/scheduler_stub.go b/plugins/pdk/go/scheduler/scheduler_stub.go new file mode 100644 index 000000000..e9eccece5 --- /dev/null +++ b/plugins/pdk/go/scheduler/scheduler_stub.go @@ -0,0 +1,52 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package scheduler + +// SchedulerCallbackOutput represents the SchedulerCallbackOutput data structure. +// SchedulerCallbackOutput is the output from the scheduler callback. +type SchedulerCallbackOutput struct { + // Error is the error message if the callback failed to process the scheduled task. + // Empty or null indicates success. The error is logged but does not + // affect the scheduling system. + Error *string `json:"error,omitempty"` +} + +// SchedulerCallbackInput represents the SchedulerCallbackInput data structure. +// SchedulerCallbackInput is the input provided when a scheduled task fires. +type SchedulerCallbackInput struct { + // ScheduleID is the unique identifier for this scheduled task. + // This is either the ID provided when scheduling, or an auto-generated UUID if none was specified. + ScheduleID string `json:"scheduleId"` + // Payload is the payload data that was provided when the task was scheduled. + // Can be used to pass context or parameters to the callback handler. + Payload string `json:"payload"` + // IsRecurring is true if this is a recurring schedule (created via ScheduleRecurring), + // false if it's a one-time schedule (created via ScheduleOneTime). + IsRecurring bool `json:"isRecurring"` +} + +// Scheduler is the marker interface for scheduler plugins. +// Implement one or more of the provider interfaces below. +// SchedulerCallback provides scheduled task handling. +// This capability allows plugins to receive callbacks when their scheduled tasks execute. +// Plugins that use the scheduler host service must implement this capability +// to handle task execution. +type Scheduler interface{} + +// SchedulerCallbackProvider provides the OnSchedulerCallback function. +type SchedulerCallbackProvider interface { + OnSchedulerCallback(SchedulerCallbackInput) (SchedulerCallbackOutput, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ Scheduler) {} diff --git a/plugins/pdk/go/scrobbler/scrobbler.go b/plugins/pdk/go/scrobbler/scrobbler.go new file mode 100644 index 000000000..cd5629698 --- /dev/null +++ b/plugins/pdk/go/scrobbler/scrobbler.go @@ -0,0 +1,224 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the Scrobbler capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package scrobbler + +import ( + pdk "github.com/extism/go-pdk" +) + +// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors. +type ScrobblerErrorType string + +const ( + // ScrobblerErrorNone indicates no error occurred. + ScrobblerErrorNone = "none" + // ScrobblerErrorNotAuthorized indicates the user is not authorized. + ScrobblerErrorNotAuthorized = "not_authorized" + // ScrobblerErrorRetryLater indicates the operation should be retried later. + ScrobblerErrorRetryLater = "retry_later" + // ScrobblerErrorUnrecoverable indicates an unrecoverable error. + ScrobblerErrorUnrecoverable = "unrecoverable" +) + +// TrackInfo represents the TrackInfo data structure. +// TrackInfo contains track metadata for scrobbling. +type TrackInfo struct { + // ID is the internal Navidrome track ID. + ID string `json:"id"` + // Title is the track title. + Title string `json:"title"` + // Album is the album name. + Album string `json:"album"` + // Artist is the track artist. + Artist string `json:"artist"` + // AlbumArtist is the album artist. + AlbumArtist string `json:"albumArtist"` + // Duration is the track duration in seconds. + Duration float64 `json:"duration"` + // TrackNumber is the track number on the album. + TrackNumber int32 `json:"trackNumber"` + // DiscNumber is the disc number. + DiscNumber int32 `json:"discNumber"` + // MBZRecordingID is the MusicBrainz recording ID. + MBZRecordingID *string `json:"mbzRecordingId,omitempty"` + // MBZAlbumID is the MusicBrainz album/release ID. + MBZAlbumID *string `json:"mbzAlbumId,omitempty"` + // MBZArtistID is the MusicBrainz artist ID. + MBZArtistID *string `json:"mbzArtistId,omitempty"` + // MBZReleaseGroupID is the MusicBrainz release group ID. + MBZReleaseGroupID *string `json:"mbzReleaseGroupId,omitempty"` + // MBZAlbumArtistID is the MusicBrainz album artist ID. + MBZAlbumArtistID *string `json:"mbzAlbumArtistId,omitempty"` + // MBZReleaseTrackID is the MusicBrainz release track ID. + MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"` +} + +// AuthInput represents the AuthInput data structure. +// AuthInput is the input for authorization check. +type AuthInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` +} + +// AuthOutput represents the AuthOutput data structure. +// AuthOutput is the output for authorization check. +type AuthOutput struct { + // Authorized indicates whether the user is authorized to scrobble. + Authorized bool `json:"authorized"` +} + +// NowPlayingInput represents the NowPlayingInput data structure. +// NowPlayingInput is the input for now playing notification. +type NowPlayingInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` + // Track is the track currently playing. + Track TrackInfo `json:"track"` + // Position is the current playback position in seconds. + Position int32 `json:"position"` +} + +// ScrobblerOutput represents the ScrobblerOutput data structure. +// ScrobblerOutput is the output for scrobbler operations. +type ScrobblerOutput struct { + // Error is the error message if the operation failed. + Error *string `json:"error,omitempty"` + // ErrorType indicates how Navidrome should handle the error. + ErrorType *ScrobblerErrorType `json:"errorType,omitempty"` +} + +// ScrobbleInput represents the ScrobbleInput data structure. +// ScrobbleInput is the input for submitting a scrobble. +type ScrobbleInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` + // Track is the track that was played. + Track TrackInfo `json:"track"` + // Timestamp is the Unix timestamp when the track started playing. + Timestamp int64 `json:"timestamp"` +} + +// Scrobbler requires all methods to be implemented. +// Scrobbler provides scrobbling functionality to external services. +// This capability allows plugins to submit listening history to services like Last.fm, +// ListenBrainz, or custom scrobbling backends. +// +// All methods are required - plugins implementing this capability must provide +// all three functions: IsAuthorized, NowPlaying, and Scrobble. +type Scrobbler interface { + // IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service. + IsAuthorized(AuthInput) (AuthOutput, error) + // NowPlaying - NowPlaying sends a now playing notification to the scrobbling service. + NowPlaying(NowPlayingInput) (ScrobblerOutput, error) + // Scrobble - Scrobble submits a completed scrobble to the scrobbling service. + Scrobble(ScrobbleInput) (ScrobblerOutput, error) +} // Internal implementation holders +var ( + isAuthorizedImpl func(AuthInput) (AuthOutput, error) + nowPlayingImpl func(NowPlayingInput) (ScrobblerOutput, error) + scrobbleImpl func(ScrobbleInput) (ScrobblerOutput, error) +) + +// Register registers a scrobbler implementation. +// All methods are required. +func Register(impl Scrobbler) { + isAuthorizedImpl = impl.IsAuthorized + nowPlayingImpl = impl.NowPlaying + scrobbleImpl = impl.Scrobble +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//export nd_scrobbler_is_authorized +func _NdScrobblerIsAuthorized() int32 { + if isAuthorizedImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input AuthInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := isAuthorizedImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_scrobbler_now_playing +func _NdScrobblerNowPlaying() int32 { + if nowPlayingImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input NowPlayingInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := nowPlayingImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_scrobbler_scrobble +func _NdScrobblerScrobble() int32 { + if scrobbleImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input ScrobbleInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := scrobbleImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/scrobbler/scrobbler_stub.go b/plugins/pdk/go/scrobbler/scrobbler_stub.go new file mode 100644 index 000000000..88d6c3537 --- /dev/null +++ b/plugins/pdk/go/scrobbler/scrobbler_stub.go @@ -0,0 +1,130 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package scrobbler + +// ScrobblerErrorType indicates how Navidrome should handle scrobbler errors. +type ScrobblerErrorType string + +const ( + // ScrobblerErrorNone indicates no error occurred. + ScrobblerErrorNone = "none" + // ScrobblerErrorNotAuthorized indicates the user is not authorized. + ScrobblerErrorNotAuthorized = "not_authorized" + // ScrobblerErrorRetryLater indicates the operation should be retried later. + ScrobblerErrorRetryLater = "retry_later" + // ScrobblerErrorUnrecoverable indicates an unrecoverable error. + ScrobblerErrorUnrecoverable = "unrecoverable" +) + +// TrackInfo represents the TrackInfo data structure. +// TrackInfo contains track metadata for scrobbling. +type TrackInfo struct { + // ID is the internal Navidrome track ID. + ID string `json:"id"` + // Title is the track title. + Title string `json:"title"` + // Album is the album name. + Album string `json:"album"` + // Artist is the track artist. + Artist string `json:"artist"` + // AlbumArtist is the album artist. + AlbumArtist string `json:"albumArtist"` + // Duration is the track duration in seconds. + Duration float64 `json:"duration"` + // TrackNumber is the track number on the album. + TrackNumber int32 `json:"trackNumber"` + // DiscNumber is the disc number. + DiscNumber int32 `json:"discNumber"` + // MBZRecordingID is the MusicBrainz recording ID. + MBZRecordingID *string `json:"mbzRecordingId,omitempty"` + // MBZAlbumID is the MusicBrainz album/release ID. + MBZAlbumID *string `json:"mbzAlbumId,omitempty"` + // MBZArtistID is the MusicBrainz artist ID. + MBZArtistID *string `json:"mbzArtistId,omitempty"` + // MBZReleaseGroupID is the MusicBrainz release group ID. + MBZReleaseGroupID *string `json:"mbzReleaseGroupId,omitempty"` + // MBZAlbumArtistID is the MusicBrainz album artist ID. + MBZAlbumArtistID *string `json:"mbzAlbumArtistId,omitempty"` + // MBZReleaseTrackID is the MusicBrainz release track ID. + MBZReleaseTrackID *string `json:"mbzReleaseTrackId,omitempty"` +} + +// AuthInput represents the AuthInput data structure. +// AuthInput is the input for authorization check. +type AuthInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` +} + +// AuthOutput represents the AuthOutput data structure. +// AuthOutput is the output for authorization check. +type AuthOutput struct { + // Authorized indicates whether the user is authorized to scrobble. + Authorized bool `json:"authorized"` +} + +// NowPlayingInput represents the NowPlayingInput data structure. +// NowPlayingInput is the input for now playing notification. +type NowPlayingInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` + // Track is the track currently playing. + Track TrackInfo `json:"track"` + // Position is the current playback position in seconds. + Position int32 `json:"position"` +} + +// ScrobblerOutput represents the ScrobblerOutput data structure. +// ScrobblerOutput is the output for scrobbler operations. +type ScrobblerOutput struct { + // Error is the error message if the operation failed. + Error *string `json:"error,omitempty"` + // ErrorType indicates how Navidrome should handle the error. + ErrorType *ScrobblerErrorType `json:"errorType,omitempty"` +} + +// ScrobbleInput represents the ScrobbleInput data structure. +// ScrobbleInput is the input for submitting a scrobble. +type ScrobbleInput struct { + // UserID is the internal Navidrome user ID. + UserID string `json:"userId"` + // Username is the username of the user. + Username string `json:"username"` + // Track is the track that was played. + Track TrackInfo `json:"track"` + // Timestamp is the Unix timestamp when the track started playing. + Timestamp int64 `json:"timestamp"` +} + +// Scrobbler requires all methods to be implemented. +// Scrobbler provides scrobbling functionality to external services. +// This capability allows plugins to submit listening history to services like Last.fm, +// ListenBrainz, or custom scrobbling backends. +// +// All methods are required - plugins implementing this capability must provide +// all three functions: IsAuthorized, NowPlaying, and Scrobble. +type Scrobbler interface { + // IsAuthorized - IsAuthorized checks if a user is authorized to scrobble to this service. + IsAuthorized(AuthInput) (AuthOutput, error) + // NowPlaying - NowPlaying sends a now playing notification to the scrobbling service. + NowPlaying(NowPlayingInput) (ScrobblerOutput, error) + // Scrobble - Scrobble submits a completed scrobble to the scrobbling service. + Scrobble(ScrobbleInput) (ScrobblerOutput, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ Scrobbler) {} diff --git a/plugins/pdk/go/websocket/websocket.go b/plugins/pdk/go/websocket/websocket.go new file mode 100644 index 000000000..2653ae09b --- /dev/null +++ b/plugins/pdk/go/websocket/websocket.go @@ -0,0 +1,247 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file contains export wrappers for the WebSocketCallback capability. +// It is intended for use in Navidrome plugins built with TinyGo. +// +//go:build wasip1 + +package websocket + +import ( + pdk "github.com/extism/go-pdk" +) + +// OnErrorInput represents the OnErrorInput data structure. +// OnErrorInput is the input provided when an error occurs on a WebSocket connection. +type OnErrorInput struct { + // ConnectionID is the unique identifier for the WebSocket connection where the error occurred. + ConnectionID string `json:"connectionId"` + // Error is the error message describing what went wrong. + Error string `json:"error"` +} + +// OnErrorOutput represents the OnErrorOutput data structure. +// OnErrorOutput is the output from the error handler. +type OnErrorOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnCloseInput represents the OnCloseInput data structure. +// OnCloseInput is the input provided when a WebSocket connection is closed. +type OnCloseInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that was closed. + ConnectionID string `json:"connectionId"` + // Code is the WebSocket close status code (e.g., 1000 for normal closure, + // 1001 for going away, 1006 for abnormal closure). + Code int32 `json:"code"` + // Reason is the human-readable reason for the connection closure, if provided. + Reason string `json:"reason"` +} + +// OnCloseOutput represents the OnCloseOutput data structure. +// OnCloseOutput is the output from the close handler. +type OnCloseOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnTextMessageInput represents the OnTextMessageInput data structure. +// OnTextMessageInput is the input provided when a text message is received. +type OnTextMessageInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that received the message. + ConnectionID string `json:"connectionId"` + // Message is the text message content received from the WebSocket. + Message string `json:"message"` +} + +// OnTextMessageOutput represents the OnTextMessageOutput data structure. +// OnTextMessageOutput is the output from the text message handler. +type OnTextMessageOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnBinaryMessageInput represents the OnBinaryMessageInput data structure. +// OnBinaryMessageInput is the input provided when a binary message is received. +type OnBinaryMessageInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that received the message. + ConnectionID string `json:"connectionId"` + // Data is the binary data received from the WebSocket, encoded as base64. + Data string `json:"data"` +} + +// OnBinaryMessageOutput represents the OnBinaryMessageOutput data structure. +// OnBinaryMessageOutput is the output from the binary message handler. +type OnBinaryMessageOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// WebSocket is the marker interface for websocket plugins. +// Implement one or more of the provider interfaces below. +// WebSocketCallback provides WebSocket message handling. +// This capability allows plugins to receive callbacks for WebSocket events +// such as text messages, binary messages, errors, and connection closures. +// Plugins that use the WebSocket host service must implement this capability +// to handle incoming events. +type WebSocket interface{} + +// TextMessageProvider provides the OnTextMessage function. +type TextMessageProvider interface { + OnTextMessage(OnTextMessageInput) (OnTextMessageOutput, error) +} + +// BinaryMessageProvider provides the OnBinaryMessage function. +type BinaryMessageProvider interface { + OnBinaryMessage(OnBinaryMessageInput) (OnBinaryMessageOutput, error) +} + +// ErrorProvider provides the OnError function. +type ErrorProvider interface { + OnError(OnErrorInput) (OnErrorOutput, error) +} + +// CloseProvider provides the OnClose function. +type CloseProvider interface { + OnClose(OnCloseInput) (OnCloseOutput, error) +} // Internal implementation holders +var ( + textMessageImpl func(OnTextMessageInput) (OnTextMessageOutput, error) + binaryMessageImpl func(OnBinaryMessageInput) (OnBinaryMessageOutput, error) + errorImpl func(OnErrorInput) (OnErrorOutput, error) + closeImpl func(OnCloseInput) (OnCloseOutput, error) +) + +// Register registers a websocket implementation. +// The implementation is checked for optional provider interfaces. +func Register(impl WebSocket) { + if p, ok := impl.(TextMessageProvider); ok { + textMessageImpl = p.OnTextMessage + } + if p, ok := impl.(BinaryMessageProvider); ok { + binaryMessageImpl = p.OnBinaryMessage + } + if p, ok := impl.(ErrorProvider); ok { + errorImpl = p.OnError + } + if p, ok := impl.(CloseProvider); ok { + closeImpl = p.OnClose + } +} + +// NotImplementedCode is the standard return code for unimplemented functions. +// The host recognizes this and skips the plugin gracefully. +const NotImplementedCode int32 = -2 + +//export nd_websocket_on_text_message +func _NdWebsocketOnTextMessage() int32 { + if textMessageImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input OnTextMessageInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := textMessageImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_websocket_on_binary_message +func _NdWebsocketOnBinaryMessage() int32 { + if binaryMessageImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input OnBinaryMessageInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := binaryMessageImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_websocket_on_error +func _NdWebsocketOnError() int32 { + if errorImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input OnErrorInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := errorImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} + +//export nd_websocket_on_close +func _NdWebsocketOnClose() int32 { + if closeImpl == nil { + // Return standard code - host will skip this plugin gracefully + return NotImplementedCode + } + + var input OnCloseInput + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(err) + return -1 + } + + output, err := closeImpl(input) + if err != nil { + pdk.SetError(err) + return -1 + } + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(err) + return -1 + } + + return 0 +} diff --git a/plugins/pdk/go/websocket/websocket_stub.go b/plugins/pdk/go/websocket/websocket_stub.go new file mode 100644 index 000000000..c9b4d02a7 --- /dev/null +++ b/plugins/pdk/go/websocket/websocket_stub.go @@ -0,0 +1,116 @@ +// Code generated by ndpgen. DO NOT EDIT. +// +// This file provides stub implementations for non-WASM platforms. +// It allows Go plugins to compile and run tests outside of WASM, +// but the actual functionality is only available in WASM builds. +// +//go:build !wasip1 + +package websocket + +// OnErrorInput represents the OnErrorInput data structure. +// OnErrorInput is the input provided when an error occurs on a WebSocket connection. +type OnErrorInput struct { + // ConnectionID is the unique identifier for the WebSocket connection where the error occurred. + ConnectionID string `json:"connectionId"` + // Error is the error message describing what went wrong. + Error string `json:"error"` +} + +// OnErrorOutput represents the OnErrorOutput data structure. +// OnErrorOutput is the output from the error handler. +type OnErrorOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnCloseInput represents the OnCloseInput data structure. +// OnCloseInput is the input provided when a WebSocket connection is closed. +type OnCloseInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that was closed. + ConnectionID string `json:"connectionId"` + // Code is the WebSocket close status code (e.g., 1000 for normal closure, + // 1001 for going away, 1006 for abnormal closure). + Code int32 `json:"code"` + // Reason is the human-readable reason for the connection closure, if provided. + Reason string `json:"reason"` +} + +// OnCloseOutput represents the OnCloseOutput data structure. +// OnCloseOutput is the output from the close handler. +type OnCloseOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnTextMessageInput represents the OnTextMessageInput data structure. +// OnTextMessageInput is the input provided when a text message is received. +type OnTextMessageInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that received the message. + ConnectionID string `json:"connectionId"` + // Message is the text message content received from the WebSocket. + Message string `json:"message"` +} + +// OnTextMessageOutput represents the OnTextMessageOutput data structure. +// OnTextMessageOutput is the output from the text message handler. +type OnTextMessageOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// OnBinaryMessageInput represents the OnBinaryMessageInput data structure. +// OnBinaryMessageInput is the input provided when a binary message is received. +type OnBinaryMessageInput struct { + // ConnectionID is the unique identifier for the WebSocket connection that received the message. + ConnectionID string `json:"connectionId"` + // Data is the binary data received from the WebSocket, encoded as base64. + Data string `json:"data"` +} + +// OnBinaryMessageOutput represents the OnBinaryMessageOutput data structure. +// OnBinaryMessageOutput is the output from the binary message handler. +type OnBinaryMessageOutput struct { + // Error is the error message if the callback failed. + // Empty or null indicates success. + Error *string `json:"error,omitempty"` +} + +// WebSocket is the marker interface for websocket plugins. +// Implement one or more of the provider interfaces below. +// WebSocketCallback provides WebSocket message handling. +// This capability allows plugins to receive callbacks for WebSocket events +// such as text messages, binary messages, errors, and connection closures. +// Plugins that use the WebSocket host service must implement this capability +// to handle incoming events. +type WebSocket interface{} + +// TextMessageProvider provides the OnTextMessage function. +type TextMessageProvider interface { + OnTextMessage(OnTextMessageInput) (OnTextMessageOutput, error) +} + +// BinaryMessageProvider provides the OnBinaryMessage function. +type BinaryMessageProvider interface { + OnBinaryMessage(OnBinaryMessageInput) (OnBinaryMessageOutput, error) +} + +// ErrorProvider provides the OnError function. +type ErrorProvider interface { + OnError(OnErrorInput) (OnErrorOutput, error) +} + +// CloseProvider provides the OnClose function. +type CloseProvider interface { + OnClose(OnCloseInput) (OnCloseOutput, error) +} + +// NotImplementedCode is the standard return code for unimplemented functions. +const NotImplementedCode int32 = -2 + +// Register is a no-op on non-WASM platforms. +// This stub allows code to compile outside of WASM. +func Register(_ WebSocket) {}