diff --git a/README.md b/README.md index ebad6e4..ef8dd3c 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,37 @@ err = host.CallPluginJSON(ctx, runtimehost.CallPluginJSONRequest{ The `auth_provider.v1` capability also exposes OAuth-flow RPCs (`InitAuthorize`, `ExchangeCode`, `RefreshSession`) for plugins that wrap external identity providers. +## Request routers + +`request_router.v1` lets the host hand a media request to a download backend +such as Sonarr, Radarr, or Seerr. The host owns the request lifecycle, policy, +and quality governance; the plugin routes the request to a configured +connection and reports its status. + +A series request may name seasons in `RequestDescriptor.seasons`. Season `0` +is Specials, and an empty list means the whole series. A plugin that fulfils +seasons individually declares it in its manifest: + +```json +{ + "type": "request_router.v1", + "id": "arr", + "request_router": { "supports_seasons": true } +} +``` + +A declaring plugin must acquire only the requested seasons. When the series +already exists upstream, it adds those seasons to what is already tracked and +leaves the other seasons alone, so a request for season 4 never stops tracking +seasons 1–3. Repeating a request must converge rather than add the series +again. `CheckStatus` receives the same descriptor, so status can cover the +requested seasons. + +Plugins without the flag, including every plugin built before it existed, keep +today's whole-series behaviour. The host requests only the missing seasons of a +series it already has from plugins that declare `supports_seasons`, because +any other plugin would add the whole series again. + ## Watch sync providers `watch_sync_provider.v1` lets external plugins participate in Silo's host-owned diff --git a/docs/compatibility.md b/docs/compatibility.md index 953996c..9e28692 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -68,6 +68,14 @@ Watch-sync rating fields (`WatchSyncEvent.rating` and ratings run from 1 to 10, so zero never carries a rating and no presence check is needed. +`RequestDescriptor.seasons` is a repeated field, so it has no presence: an +empty list means the whole series, and season `0` in a non-empty list means +Specials. A plugin built before the field existed decodes it as an unknown +field and fulfils the whole series. Because the plugin cannot say so on the +wire, the host decides who may receive a season-only request from the +manifest's `RequestRouterDescriptor.supports_seasons` flag instead. An absent +descriptor means the flag is false. + A season-scoped `GetImagesRequest` is a scope, not a guarantee. Plugins that can filter by season should do so, and plugins should populate `ImageRecord.season_number` whenever the season is known. Hosts must bucket and diff --git a/pkg/pluginproto/silo/plugin/v1/common.pb.go b/pkg/pluginproto/silo/plugin/v1/common.pb.go index 267874d..ba74bcd 100644 --- a/pkg/pluginproto/silo/plugin/v1/common.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/common.pb.go @@ -981,8 +981,12 @@ type CapabilityDescriptor struct { // of type "network_access_provider.v1". The host lists available overlay // providers from this descriptor without launching the plugin. NetworkAccessProvider *NetworkAccessProviderDescriptor `protobuf:"bytes,11,opt,name=network_access_provider,json=networkAccessProvider,proto3" json:"network_access_provider,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // Typed request-router contract metadata. Only meaningful for capabilities + // of type "request_router.v1". Optional: an absent descriptor declares no + // optional request-router features. + RequestRouter *RequestRouterDescriptor `protobuf:"bytes,12,opt,name=request_router,json=requestRouter,proto3" json:"request_router,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CapabilityDescriptor) Reset() { @@ -1092,6 +1096,13 @@ func (x *CapabilityDescriptor) GetNetworkAccessProvider() *NetworkAccessProvider return nil } +func (x *CapabilityDescriptor) GetRequestRouter() *RequestRouterDescriptor { + if x != nil { + return x.RequestRouter + } + return nil +} + // PluginPresentation describes a plugin for server operators and links them to // its publisher, source, support, and release history. These values are // self-declared by the plugin and never determine catalog approval. @@ -1610,7 +1621,7 @@ var File_silo_plugin_v1_common_proto protoreflect.FileDescriptor const file_silo_plugin_v1_common_proto_rawDesc = "" + "\n" + - "\x1bsilo/plugin/v1/common.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a,silo/plugin/v1/network_access_provider.proto\x1a(silo/plugin/v1/watch_sync_provider.proto\"7\n" + + "\x1bsilo/plugin/v1/common.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\x1a,silo/plugin/v1/network_access_provider.proto\x1a#silo/plugin/v1/request_router.proto\x1a(silo/plugin/v1/watch_sync_provider.proto\"7\n" + "\x11SupportedPlatform\x12\x0e\n" + "\x02os\x18\x01 \x01(\tR\x02os\x12\x12\n" + "\x04arch\x18\x02 \x01(\tR\x04arch\"\xd9\x01\n" + @@ -1687,7 +1698,7 @@ const file_silo_plugin_v1_common_proto_rawDesc = "" + "\rPackagedAsset\x12\x12\n" + "\x04path\x18\x01 \x01(\tR\x04path\x12!\n" + "\fcontent_type\x18\x02 \x01(\tR\vcontentType\x12\x1c\n" + - "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\x9d\x04\n" + + "\tintegrity\x18\x03 \x01(\tR\tintegrity\"\xed\x04\n" + "\x14CapabilityDescriptor\x12\x12\n" + "\x04type\x18\x01 \x01(\tR\x04type\x12\x0e\n" + "\x02id\x18\x02 \x01(\tR\x02id\x12!\n" + @@ -1701,7 +1712,8 @@ const file_silo_plugin_v1_common_proto_rawDesc = "" + "\bicon_url\x18\t \x01(\tR\aiconUrl\x12[\n" + "\x13watch_sync_provider\x18\n" + " \x01(\v2+.silo.plugin.v1.WatchSyncProviderDescriptorR\x11watchSyncProvider\x12g\n" + - "\x17network_access_provider\x18\v \x01(\v2/.silo.plugin.v1.NetworkAccessProviderDescriptorR\x15networkAccessProvider\"\xa2\x03\n" + + "\x17network_access_provider\x18\v \x01(\v2/.silo.plugin.v1.NetworkAccessProviderDescriptorR\x15networkAccessProvider\x12N\n" + + "\x0erequest_router\x18\f \x01(\v2'.silo.plugin.v1.RequestRouterDescriptorR\rrequestRouter\"\xa2\x03\n" + "\x12PluginPresentation\x12!\n" + "\fdisplay_name\x18\x01 \x01(\tR\vdisplayName\x12\x18\n" + "\asummary\x18\x02 \x01(\tR\asummary\x121\n" + @@ -1796,6 +1808,7 @@ var file_silo_plugin_v1_common_proto_goTypes = []any{ (*structpb.Struct)(nil), // 22: google.protobuf.Struct (*WatchSyncProviderDescriptor)(nil), // 23: silo.plugin.v1.WatchSyncProviderDescriptor (*NetworkAccessProviderDescriptor)(nil), // 24: silo.plugin.v1.NetworkAccessProviderDescriptor + (*RequestRouterDescriptor)(nil), // 25: silo.plugin.v1.RequestRouterDescriptor } var file_silo_plugin_v1_common_proto_depIdxs = []int32{ 8, // 0: silo.plugin.v1.ConfigSchema.admin_form:type_name -> silo.plugin.v1.AdminFormDescriptor @@ -1812,27 +1825,28 @@ var file_silo_plugin_v1_common_proto_depIdxs = []int32{ 22, // 11: silo.plugin.v1.CapabilityDescriptor.metadata:type_name -> google.protobuf.Struct 23, // 12: silo.plugin.v1.CapabilityDescriptor.watch_sync_provider:type_name -> silo.plugin.v1.WatchSyncProviderDescriptor 24, // 13: silo.plugin.v1.CapabilityDescriptor.network_access_provider:type_name -> silo.plugin.v1.NetworkAccessProviderDescriptor - 1, // 14: silo.plugin.v1.PluginManifest.supported_platforms:type_name -> silo.plugin.v1.SupportedPlatform - 12, // 15: silo.plugin.v1.PluginManifest.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor - 2, // 16: silo.plugin.v1.PluginManifest.global_config_schema:type_name -> silo.plugin.v1.ConfigSchema - 2, // 17: silo.plugin.v1.PluginManifest.user_config_schema:type_name -> silo.plugin.v1.ConfigSchema - 10, // 18: silo.plugin.v1.PluginManifest.http_routes:type_name -> silo.plugin.v1.HttpRouteDescriptor - 11, // 19: silo.plugin.v1.PluginManifest.assets:type_name -> silo.plugin.v1.PackagedAsset - 22, // 20: silo.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct - 13, // 21: silo.plugin.v1.PluginManifest.presentation:type_name -> silo.plugin.v1.PluginPresentation - 14, // 22: silo.plugin.v1.GetManifestResponse.manifest:type_name -> silo.plugin.v1.PluginManifest - 9, // 23: silo.plugin.v1.ConfigureRequest.config:type_name -> silo.plugin.v1.ConfigEntry - 15, // 24: silo.plugin.v1.Runtime.GetManifest:input_type -> silo.plugin.v1.GetManifestRequest - 17, // 25: silo.plugin.v1.Runtime.Configure:input_type -> silo.plugin.v1.ConfigureRequest - 19, // 26: silo.plugin.v1.Runtime.BindHostBroker:input_type -> silo.plugin.v1.BindHostBrokerRequest - 16, // 27: silo.plugin.v1.Runtime.GetManifest:output_type -> silo.plugin.v1.GetManifestResponse - 18, // 28: silo.plugin.v1.Runtime.Configure:output_type -> silo.plugin.v1.ConfigureResponse - 20, // 29: silo.plugin.v1.Runtime.BindHostBroker:output_type -> silo.plugin.v1.BindHostBrokerResponse - 27, // [27:30] is the sub-list for method output_type - 24, // [24:27] is the sub-list for method input_type - 24, // [24:24] is the sub-list for extension type_name - 24, // [24:24] is the sub-list for extension extendee - 0, // [0:24] is the sub-list for field type_name + 25, // 14: silo.plugin.v1.CapabilityDescriptor.request_router:type_name -> silo.plugin.v1.RequestRouterDescriptor + 1, // 15: silo.plugin.v1.PluginManifest.supported_platforms:type_name -> silo.plugin.v1.SupportedPlatform + 12, // 16: silo.plugin.v1.PluginManifest.capabilities:type_name -> silo.plugin.v1.CapabilityDescriptor + 2, // 17: silo.plugin.v1.PluginManifest.global_config_schema:type_name -> silo.plugin.v1.ConfigSchema + 2, // 18: silo.plugin.v1.PluginManifest.user_config_schema:type_name -> silo.plugin.v1.ConfigSchema + 10, // 19: silo.plugin.v1.PluginManifest.http_routes:type_name -> silo.plugin.v1.HttpRouteDescriptor + 11, // 20: silo.plugin.v1.PluginManifest.assets:type_name -> silo.plugin.v1.PackagedAsset + 22, // 21: silo.plugin.v1.PluginManifest.metadata:type_name -> google.protobuf.Struct + 13, // 22: silo.plugin.v1.PluginManifest.presentation:type_name -> silo.plugin.v1.PluginPresentation + 14, // 23: silo.plugin.v1.GetManifestResponse.manifest:type_name -> silo.plugin.v1.PluginManifest + 9, // 24: silo.plugin.v1.ConfigureRequest.config:type_name -> silo.plugin.v1.ConfigEntry + 15, // 25: silo.plugin.v1.Runtime.GetManifest:input_type -> silo.plugin.v1.GetManifestRequest + 17, // 26: silo.plugin.v1.Runtime.Configure:input_type -> silo.plugin.v1.ConfigureRequest + 19, // 27: silo.plugin.v1.Runtime.BindHostBroker:input_type -> silo.plugin.v1.BindHostBrokerRequest + 16, // 28: silo.plugin.v1.Runtime.GetManifest:output_type -> silo.plugin.v1.GetManifestResponse + 18, // 29: silo.plugin.v1.Runtime.Configure:output_type -> silo.plugin.v1.ConfigureResponse + 20, // 30: silo.plugin.v1.Runtime.BindHostBroker:output_type -> silo.plugin.v1.BindHostBrokerResponse + 28, // [28:31] is the sub-list for method output_type + 25, // [25:28] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_silo_plugin_v1_common_proto_init() } @@ -1841,6 +1855,7 @@ func file_silo_plugin_v1_common_proto_init() { return } file_silo_plugin_v1_network_access_provider_proto_init() + file_silo_plugin_v1_request_router_proto_init() file_silo_plugin_v1_watch_sync_provider_proto_init() type x struct{} out := protoimpl.TypeBuilder{ diff --git a/pkg/pluginproto/silo/plugin/v1/request_router.pb.go b/pkg/pluginproto/silo/plugin/v1/request_router.pb.go index e30d83a..6ef7377 100644 --- a/pkg/pluginproto/silo/plugin/v1/request_router.pb.go +++ b/pkg/pluginproto/silo/plugin/v1/request_router.pb.go @@ -35,8 +35,14 @@ type RequestDescriptor struct { // on the downstream service (e.g. Seerr). Empty when the host cannot resolve it. RequesterEmail string `protobuf:"bytes,8,opt,name=requester_email,json=requesterEmail,proto3" json:"requester_email,omitempty"` RequesterUsername string `protobuf:"bytes,9,opt,name=requester_username,json=requesterUsername,proto3" json:"requester_username,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // seasons lists the requested season numbers of a series; 0 is Specials. + // Empty means the whole series, which is what every request meant before + // this field existed. Only set when media_type is "series". Plugins that + // declare RequestRouterDescriptor.supports_seasons must fulfil only these + // seasons; see that field for the full contract. + Seasons []int32 `protobuf:"varint,10,rep,packed,name=seasons,proto3" json:"seasons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RequestDescriptor) Reset() { @@ -132,6 +138,68 @@ func (x *RequestDescriptor) GetRequesterUsername() string { return "" } +func (x *RequestDescriptor) GetSeasons() []int32 { + if x != nil { + return x.Seasons + } + return nil +} + +// RequestRouterDescriptor is typed request_router.v1 contract metadata that the +// host reads from the manifest without launching the plugin. An absent +// descriptor means every flag is false, which is how the host treats plugins +// built before a flag existed. +type RequestRouterDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // supports_seasons declares that Fulfill and CheckStatus honour + // RequestDescriptor.seasons. A declaring plugin acquires only the listed + // seasons; when the series already exists upstream it adds those seasons to + // what is already tracked and leaves other seasons unchanged. Repeating a + // request converges rather than duplicating work. The host sends a request + // for only the missing seasons of a series it already has only to plugins + // that set this flag, because other plugins would add the whole series. + SupportsSeasons bool `protobuf:"varint,1,opt,name=supports_seasons,json=supportsSeasons,proto3" json:"supports_seasons,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RequestRouterDescriptor) Reset() { + *x = RequestRouterDescriptor{} + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RequestRouterDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestRouterDescriptor) ProtoMessage() {} + +func (x *RequestRouterDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestRouterDescriptor.ProtoReflect.Descriptor instead. +func (*RequestRouterDescriptor) Descriptor() ([]byte, []int) { + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{1} +} + +func (x *RequestRouterDescriptor) GetSupportsSeasons() bool { + if x != nil { + return x.SupportsSeasons + } + return false +} + type RouterConnection struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` @@ -146,7 +214,7 @@ type RouterConnection struct { func (x *RouterConnection) Reset() { *x = RouterConnection{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -158,7 +226,7 @@ func (x *RouterConnection) String() string { func (*RouterConnection) ProtoMessage() {} func (x *RouterConnection) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[1] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -171,7 +239,7 @@ func (x *RouterConnection) ProtoReflect() protoreflect.Message { // Deprecated: Use RouterConnection.ProtoReflect.Descriptor instead. func (*RouterConnection) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{1} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{2} } func (x *RouterConnection) GetId() string { @@ -216,7 +284,7 @@ type RequestedQuality struct { func (x *RequestedQuality) Reset() { *x = RequestedQuality{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -228,7 +296,7 @@ func (x *RequestedQuality) String() string { func (*RequestedQuality) ProtoMessage() {} func (x *RequestedQuality) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[2] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -241,7 +309,7 @@ func (x *RequestedQuality) ProtoReflect() protoreflect.Message { // Deprecated: Use RequestedQuality.ProtoReflect.Descriptor instead. func (*RequestedQuality) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{2} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{3} } func (x *RequestedQuality) GetId() string { @@ -270,7 +338,7 @@ type FulfillRequest struct { func (x *FulfillRequest) Reset() { *x = FulfillRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -282,7 +350,7 @@ func (x *FulfillRequest) String() string { func (*FulfillRequest) ProtoMessage() {} func (x *FulfillRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[3] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -295,7 +363,7 @@ func (x *FulfillRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillRequest.ProtoReflect.Descriptor instead. func (*FulfillRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{3} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{4} } func (x *FulfillRequest) GetCapabilityId() string { @@ -343,7 +411,7 @@ type FulfillmentTarget struct { func (x *FulfillmentTarget) Reset() { *x = FulfillmentTarget{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -355,7 +423,7 @@ func (x *FulfillmentTarget) String() string { func (*FulfillmentTarget) ProtoMessage() {} func (x *FulfillmentTarget) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[4] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -368,7 +436,7 @@ func (x *FulfillmentTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillmentTarget.ProtoReflect.Descriptor instead. func (*FulfillmentTarget) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{4} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{5} } func (x *FulfillmentTarget) GetQuality() string { @@ -424,7 +492,7 @@ type FulfillResponse struct { func (x *FulfillResponse) Reset() { *x = FulfillResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -436,7 +504,7 @@ func (x *FulfillResponse) String() string { func (*FulfillResponse) ProtoMessage() {} func (x *FulfillResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[5] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -449,7 +517,7 @@ func (x *FulfillResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FulfillResponse.ProtoReflect.Descriptor instead. func (*FulfillResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{5} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{6} } func (x *FulfillResponse) GetTargets() []*FulfillmentTarget { @@ -477,7 +545,7 @@ type TargetRef struct { func (x *TargetRef) Reset() { *x = TargetRef{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -489,7 +557,7 @@ func (x *TargetRef) String() string { func (*TargetRef) ProtoMessage() {} func (x *TargetRef) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[6] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -502,7 +570,7 @@ func (x *TargetRef) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetRef.ProtoReflect.Descriptor instead. func (*TargetRef) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{6} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{7} } func (x *TargetRef) GetQuality() string { @@ -538,7 +606,7 @@ type CheckStatusRequest struct { func (x *CheckStatusRequest) Reset() { *x = CheckStatusRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -550,7 +618,7 @@ func (x *CheckStatusRequest) String() string { func (*CheckStatusRequest) ProtoMessage() {} func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[7] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -563,7 +631,7 @@ func (x *CheckStatusRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckStatusRequest.ProtoReflect.Descriptor instead. func (*CheckStatusRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{7} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{8} } func (x *CheckStatusRequest) GetCapabilityId() string { @@ -608,7 +676,7 @@ type TargetStatus struct { func (x *TargetStatus) Reset() { *x = TargetStatus{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -620,7 +688,7 @@ func (x *TargetStatus) String() string { func (*TargetStatus) ProtoMessage() {} func (x *TargetStatus) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[8] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -633,7 +701,7 @@ func (x *TargetStatus) ProtoReflect() protoreflect.Message { // Deprecated: Use TargetStatus.ProtoReflect.Descriptor instead. func (*TargetStatus) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{8} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{9} } func (x *TargetStatus) GetQuality() string { @@ -680,7 +748,7 @@ type CheckStatusResponse struct { func (x *CheckStatusResponse) Reset() { *x = CheckStatusResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -692,7 +760,7 @@ func (x *CheckStatusResponse) String() string { func (*CheckStatusResponse) ProtoMessage() {} func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[9] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -705,7 +773,7 @@ func (x *CheckStatusResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckStatusResponse.ProtoReflect.Descriptor instead. func (*CheckStatusResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{9} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{10} } func (x *CheckStatusResponse) GetStatuses() []*TargetStatus { @@ -725,7 +793,7 @@ type ConfigOption struct { func (x *ConfigOption) Reset() { *x = ConfigOption{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -737,7 +805,7 @@ func (x *ConfigOption) String() string { func (*ConfigOption) ProtoMessage() {} func (x *ConfigOption) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[10] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -750,7 +818,7 @@ func (x *ConfigOption) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigOption.ProtoReflect.Descriptor instead. func (*ConfigOption) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{10} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{11} } func (x *ConfigOption) GetValue() string { @@ -776,7 +844,7 @@ type ConfigOptionList struct { func (x *ConfigOptionList) Reset() { *x = ConfigOptionList{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -788,7 +856,7 @@ func (x *ConfigOptionList) String() string { func (*ConfigOptionList) ProtoMessage() {} func (x *ConfigOptionList) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[11] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -801,7 +869,7 @@ func (x *ConfigOptionList) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfigOptionList.ProtoReflect.Descriptor instead. func (*ConfigOptionList) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{11} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{12} } func (x *ConfigOptionList) GetOptions() []*ConfigOption { @@ -821,7 +889,7 @@ type ListConfigOptionsRequest struct { func (x *ListConfigOptionsRequest) Reset() { *x = ListConfigOptionsRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -833,7 +901,7 @@ func (x *ListConfigOptionsRequest) String() string { func (*ListConfigOptionsRequest) ProtoMessage() {} func (x *ListConfigOptionsRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[12] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -846,7 +914,7 @@ func (x *ListConfigOptionsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConfigOptionsRequest.ProtoReflect.Descriptor instead. func (*ListConfigOptionsRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{12} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{13} } func (x *ListConfigOptionsRequest) GetCapabilityId() string { @@ -873,7 +941,7 @@ type ListConfigOptionsResponse struct { func (x *ListConfigOptionsResponse) Reset() { *x = ListConfigOptionsResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -885,7 +953,7 @@ func (x *ListConfigOptionsResponse) String() string { func (*ListConfigOptionsResponse) ProtoMessage() {} func (x *ListConfigOptionsResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[13] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -898,7 +966,7 @@ func (x *ListConfigOptionsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListConfigOptionsResponse.ProtoReflect.Descriptor instead. func (*ListConfigOptionsResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{13} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{14} } func (x *ListConfigOptionsResponse) GetOptionsByField() map[string]*ConfigOptionList { @@ -918,7 +986,7 @@ type TestConnectionRequest struct { func (x *TestConnectionRequest) Reset() { *x = TestConnectionRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -930,7 +998,7 @@ func (x *TestConnectionRequest) String() string { func (*TestConnectionRequest) ProtoMessage() {} func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[14] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -943,7 +1011,7 @@ func (x *TestConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use TestConnectionRequest.ProtoReflect.Descriptor instead. func (*TestConnectionRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{14} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{15} } func (x *TestConnectionRequest) GetCapabilityId() string { @@ -970,7 +1038,7 @@ type TestConnectionResponse struct { func (x *TestConnectionResponse) Reset() { *x = TestConnectionResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -982,7 +1050,7 @@ func (x *TestConnectionResponse) String() string { func (*TestConnectionResponse) ProtoMessage() {} func (x *TestConnectionResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[15] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -995,7 +1063,7 @@ func (x *TestConnectionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use TestConnectionResponse.ProtoReflect.Descriptor instead. func (*TestConnectionResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{15} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{16} } func (x *TestConnectionResponse) GetOk() bool { @@ -1027,7 +1095,7 @@ type ValidateRequest struct { func (x *ValidateRequest) Reset() { *x = ValidateRequest{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1039,7 +1107,7 @@ func (x *ValidateRequest) String() string { func (*ValidateRequest) ProtoMessage() {} func (x *ValidateRequest) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[16] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1052,7 +1120,7 @@ func (x *ValidateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateRequest.ProtoReflect.Descriptor instead. func (*ValidateRequest) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{16} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{17} } func (x *ValidateRequest) GetCapabilityId() string { @@ -1086,7 +1154,7 @@ type ValidateResponse struct { func (x *ValidateResponse) Reset() { *x = ValidateResponse{} - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1098,7 +1166,7 @@ func (x *ValidateResponse) String() string { func (*ValidateResponse) ProtoMessage() {} func (x *ValidateResponse) ProtoReflect() protoreflect.Message { - mi := &file_silo_plugin_v1_request_router_proto_msgTypes[17] + mi := &file_silo_plugin_v1_request_router_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1111,7 +1179,7 @@ func (x *ValidateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ValidateResponse.ProtoReflect.Descriptor instead. func (*ValidateResponse) Descriptor() ([]byte, []int) { - return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{17} + return file_silo_plugin_v1_request_router_proto_rawDescGZIP(), []int{18} } func (x *ValidateResponse) GetFieldErrors() map[string]string { @@ -1132,7 +1200,7 @@ var File_silo_plugin_v1_request_router_proto protoreflect.FileDescriptor const file_silo_plugin_v1_request_router_proto_rawDesc = "" + "\n" + - "#silo/plugin/v1/request_router.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xc4\x03\n" + + "#silo/plugin/v1/request_router.proto\x12\x0esilo.plugin.v1\x1a\x1cgoogle/protobuf/struct.proto\"\xde\x03\n" + "\x11RequestDescriptor\x12\x1d\n" + "\n" + "media_type\x18\x01 \x01(\tR\tmediaType\x12\x14\n" + @@ -1143,10 +1211,14 @@ const file_silo_plugin_v1_request_router_proto_rawDesc = "" + "\x11requester_user_id\x18\x06 \x01(\x03R\x0frequesterUserId\x120\n" + "\x14requester_profile_id\x18\a \x01(\tR\x12requesterProfileId\x12'\n" + "\x0frequester_email\x18\b \x01(\tR\x0erequesterEmail\x12-\n" + - "\x12requester_username\x18\t \x01(\tR\x11requesterUsername\x1a>\n" + + "\x12requester_username\x18\t \x01(\tR\x11requesterUsername\x12\x18\n" + + "\aseasons\x18\n" + + " \x03(\x05R\aseasons\x1a>\n" + "\x10ExternalIdsEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x87\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"D\n" + + "\x17RequestRouterDescriptor\x12)\n" + + "\x10supports_seasons\x18\x01 \x01(\bR\x0fsupportsSeasons\"\x87\x01\n" + "\x10RouterConnection\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x19\n" + "\bbase_url\x18\x02 \x01(\tR\abaseUrl\x12\x17\n" + @@ -1244,60 +1316,61 @@ func file_silo_plugin_v1_request_router_proto_rawDescGZIP() []byte { return file_silo_plugin_v1_request_router_proto_rawDescData } -var file_silo_plugin_v1_request_router_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_silo_plugin_v1_request_router_proto_msgTypes = make([]protoimpl.MessageInfo, 22) var file_silo_plugin_v1_request_router_proto_goTypes = []any{ (*RequestDescriptor)(nil), // 0: silo.plugin.v1.RequestDescriptor - (*RouterConnection)(nil), // 1: silo.plugin.v1.RouterConnection - (*RequestedQuality)(nil), // 2: silo.plugin.v1.RequestedQuality - (*FulfillRequest)(nil), // 3: silo.plugin.v1.FulfillRequest - (*FulfillmentTarget)(nil), // 4: silo.plugin.v1.FulfillmentTarget - (*FulfillResponse)(nil), // 5: silo.plugin.v1.FulfillResponse - (*TargetRef)(nil), // 6: silo.plugin.v1.TargetRef - (*CheckStatusRequest)(nil), // 7: silo.plugin.v1.CheckStatusRequest - (*TargetStatus)(nil), // 8: silo.plugin.v1.TargetStatus - (*CheckStatusResponse)(nil), // 9: silo.plugin.v1.CheckStatusResponse - (*ConfigOption)(nil), // 10: silo.plugin.v1.ConfigOption - (*ConfigOptionList)(nil), // 11: silo.plugin.v1.ConfigOptionList - (*ListConfigOptionsRequest)(nil), // 12: silo.plugin.v1.ListConfigOptionsRequest - (*ListConfigOptionsResponse)(nil), // 13: silo.plugin.v1.ListConfigOptionsResponse - (*TestConnectionRequest)(nil), // 14: silo.plugin.v1.TestConnectionRequest - (*TestConnectionResponse)(nil), // 15: silo.plugin.v1.TestConnectionResponse - (*ValidateRequest)(nil), // 16: silo.plugin.v1.ValidateRequest - (*ValidateResponse)(nil), // 17: silo.plugin.v1.ValidateResponse - nil, // 18: silo.plugin.v1.RequestDescriptor.ExternalIdsEntry - nil, // 19: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry - nil, // 20: silo.plugin.v1.ValidateResponse.FieldErrorsEntry - (*structpb.Struct)(nil), // 21: google.protobuf.Struct + (*RequestRouterDescriptor)(nil), // 1: silo.plugin.v1.RequestRouterDescriptor + (*RouterConnection)(nil), // 2: silo.plugin.v1.RouterConnection + (*RequestedQuality)(nil), // 3: silo.plugin.v1.RequestedQuality + (*FulfillRequest)(nil), // 4: silo.plugin.v1.FulfillRequest + (*FulfillmentTarget)(nil), // 5: silo.plugin.v1.FulfillmentTarget + (*FulfillResponse)(nil), // 6: silo.plugin.v1.FulfillResponse + (*TargetRef)(nil), // 7: silo.plugin.v1.TargetRef + (*CheckStatusRequest)(nil), // 8: silo.plugin.v1.CheckStatusRequest + (*TargetStatus)(nil), // 9: silo.plugin.v1.TargetStatus + (*CheckStatusResponse)(nil), // 10: silo.plugin.v1.CheckStatusResponse + (*ConfigOption)(nil), // 11: silo.plugin.v1.ConfigOption + (*ConfigOptionList)(nil), // 12: silo.plugin.v1.ConfigOptionList + (*ListConfigOptionsRequest)(nil), // 13: silo.plugin.v1.ListConfigOptionsRequest + (*ListConfigOptionsResponse)(nil), // 14: silo.plugin.v1.ListConfigOptionsResponse + (*TestConnectionRequest)(nil), // 15: silo.plugin.v1.TestConnectionRequest + (*TestConnectionResponse)(nil), // 16: silo.plugin.v1.TestConnectionResponse + (*ValidateRequest)(nil), // 17: silo.plugin.v1.ValidateRequest + (*ValidateResponse)(nil), // 18: silo.plugin.v1.ValidateResponse + nil, // 19: silo.plugin.v1.RequestDescriptor.ExternalIdsEntry + nil, // 20: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry + nil, // 21: silo.plugin.v1.ValidateResponse.FieldErrorsEntry + (*structpb.Struct)(nil), // 22: google.protobuf.Struct } var file_silo_plugin_v1_request_router_proto_depIdxs = []int32{ - 18, // 0: silo.plugin.v1.RequestDescriptor.external_ids:type_name -> silo.plugin.v1.RequestDescriptor.ExternalIdsEntry - 21, // 1: silo.plugin.v1.RouterConnection.config:type_name -> google.protobuf.Struct + 19, // 0: silo.plugin.v1.RequestDescriptor.external_ids:type_name -> silo.plugin.v1.RequestDescriptor.ExternalIdsEntry + 22, // 1: silo.plugin.v1.RouterConnection.config:type_name -> google.protobuf.Struct 0, // 2: silo.plugin.v1.FulfillRequest.request:type_name -> silo.plugin.v1.RequestDescriptor - 2, // 3: silo.plugin.v1.FulfillRequest.qualities:type_name -> silo.plugin.v1.RequestedQuality - 1, // 4: silo.plugin.v1.FulfillRequest.connections:type_name -> silo.plugin.v1.RouterConnection - 4, // 5: silo.plugin.v1.FulfillResponse.targets:type_name -> silo.plugin.v1.FulfillmentTarget + 3, // 3: silo.plugin.v1.FulfillRequest.qualities:type_name -> silo.plugin.v1.RequestedQuality + 2, // 4: silo.plugin.v1.FulfillRequest.connections:type_name -> silo.plugin.v1.RouterConnection + 5, // 5: silo.plugin.v1.FulfillResponse.targets:type_name -> silo.plugin.v1.FulfillmentTarget 0, // 6: silo.plugin.v1.CheckStatusRequest.request:type_name -> silo.plugin.v1.RequestDescriptor - 6, // 7: silo.plugin.v1.CheckStatusRequest.targets:type_name -> silo.plugin.v1.TargetRef - 1, // 8: silo.plugin.v1.CheckStatusRequest.connections:type_name -> silo.plugin.v1.RouterConnection - 8, // 9: silo.plugin.v1.CheckStatusResponse.statuses:type_name -> silo.plugin.v1.TargetStatus - 10, // 10: silo.plugin.v1.ConfigOptionList.options:type_name -> silo.plugin.v1.ConfigOption - 1, // 11: silo.plugin.v1.ListConfigOptionsRequest.connection:type_name -> silo.plugin.v1.RouterConnection - 19, // 12: silo.plugin.v1.ListConfigOptionsResponse.options_by_field:type_name -> silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry - 1, // 13: silo.plugin.v1.TestConnectionRequest.connection:type_name -> silo.plugin.v1.RouterConnection - 1, // 14: silo.plugin.v1.ValidateRequest.connection:type_name -> silo.plugin.v1.RouterConnection - 1, // 15: silo.plugin.v1.ValidateRequest.siblings:type_name -> silo.plugin.v1.RouterConnection - 20, // 16: silo.plugin.v1.ValidateResponse.field_errors:type_name -> silo.plugin.v1.ValidateResponse.FieldErrorsEntry - 11, // 17: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry.value:type_name -> silo.plugin.v1.ConfigOptionList - 3, // 18: silo.plugin.v1.RequestRouter.Fulfill:input_type -> silo.plugin.v1.FulfillRequest - 7, // 19: silo.plugin.v1.RequestRouter.CheckStatus:input_type -> silo.plugin.v1.CheckStatusRequest - 12, // 20: silo.plugin.v1.RequestRouter.ListConfigOptions:input_type -> silo.plugin.v1.ListConfigOptionsRequest - 14, // 21: silo.plugin.v1.RequestRouter.TestConnection:input_type -> silo.plugin.v1.TestConnectionRequest - 16, // 22: silo.plugin.v1.RequestRouter.Validate:input_type -> silo.plugin.v1.ValidateRequest - 5, // 23: silo.plugin.v1.RequestRouter.Fulfill:output_type -> silo.plugin.v1.FulfillResponse - 9, // 24: silo.plugin.v1.RequestRouter.CheckStatus:output_type -> silo.plugin.v1.CheckStatusResponse - 13, // 25: silo.plugin.v1.RequestRouter.ListConfigOptions:output_type -> silo.plugin.v1.ListConfigOptionsResponse - 15, // 26: silo.plugin.v1.RequestRouter.TestConnection:output_type -> silo.plugin.v1.TestConnectionResponse - 17, // 27: silo.plugin.v1.RequestRouter.Validate:output_type -> silo.plugin.v1.ValidateResponse + 7, // 7: silo.plugin.v1.CheckStatusRequest.targets:type_name -> silo.plugin.v1.TargetRef + 2, // 8: silo.plugin.v1.CheckStatusRequest.connections:type_name -> silo.plugin.v1.RouterConnection + 9, // 9: silo.plugin.v1.CheckStatusResponse.statuses:type_name -> silo.plugin.v1.TargetStatus + 11, // 10: silo.plugin.v1.ConfigOptionList.options:type_name -> silo.plugin.v1.ConfigOption + 2, // 11: silo.plugin.v1.ListConfigOptionsRequest.connection:type_name -> silo.plugin.v1.RouterConnection + 20, // 12: silo.plugin.v1.ListConfigOptionsResponse.options_by_field:type_name -> silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry + 2, // 13: silo.plugin.v1.TestConnectionRequest.connection:type_name -> silo.plugin.v1.RouterConnection + 2, // 14: silo.plugin.v1.ValidateRequest.connection:type_name -> silo.plugin.v1.RouterConnection + 2, // 15: silo.plugin.v1.ValidateRequest.siblings:type_name -> silo.plugin.v1.RouterConnection + 21, // 16: silo.plugin.v1.ValidateResponse.field_errors:type_name -> silo.plugin.v1.ValidateResponse.FieldErrorsEntry + 12, // 17: silo.plugin.v1.ListConfigOptionsResponse.OptionsByFieldEntry.value:type_name -> silo.plugin.v1.ConfigOptionList + 4, // 18: silo.plugin.v1.RequestRouter.Fulfill:input_type -> silo.plugin.v1.FulfillRequest + 8, // 19: silo.plugin.v1.RequestRouter.CheckStatus:input_type -> silo.plugin.v1.CheckStatusRequest + 13, // 20: silo.plugin.v1.RequestRouter.ListConfigOptions:input_type -> silo.plugin.v1.ListConfigOptionsRequest + 15, // 21: silo.plugin.v1.RequestRouter.TestConnection:input_type -> silo.plugin.v1.TestConnectionRequest + 17, // 22: silo.plugin.v1.RequestRouter.Validate:input_type -> silo.plugin.v1.ValidateRequest + 6, // 23: silo.plugin.v1.RequestRouter.Fulfill:output_type -> silo.plugin.v1.FulfillResponse + 10, // 24: silo.plugin.v1.RequestRouter.CheckStatus:output_type -> silo.plugin.v1.CheckStatusResponse + 14, // 25: silo.plugin.v1.RequestRouter.ListConfigOptions:output_type -> silo.plugin.v1.ListConfigOptionsResponse + 16, // 26: silo.plugin.v1.RequestRouter.TestConnection:output_type -> silo.plugin.v1.TestConnectionResponse + 18, // 27: silo.plugin.v1.RequestRouter.Validate:output_type -> silo.plugin.v1.ValidateResponse 23, // [23:28] is the sub-list for method output_type 18, // [18:23] is the sub-list for method input_type 18, // [18:18] is the sub-list for extension type_name @@ -1316,7 +1389,7 @@ func file_silo_plugin_v1_request_router_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_silo_plugin_v1_request_router_proto_rawDesc), len(file_silo_plugin_v1_request_router_proto_rawDesc)), NumEnums: 0, - NumMessages: 21, + NumMessages: 22, NumExtensions: 0, NumServices: 1, }, diff --git a/pkg/pluginsdk/convert/convert.go b/pkg/pluginsdk/convert/convert.go index 1e4fd5c..93c324c 100644 --- a/pkg/pluginsdk/convert/convert.go +++ b/pkg/pluginsdk/convert/convert.go @@ -81,6 +81,17 @@ func DecodeCapability(record CapabilityRecord) (*pluginv1.CapabilityDescriptor, } descriptor.WatchSyncProvider = &typed } + if requestRouter, ok := record.Metadata["request_router"]; ok { + data, err := json.Marshal(requestRouter) + if err != nil { + return nil, fmt.Errorf("encode request router descriptor: %w", err) + } + var typed pluginv1.RequestRouterDescriptor + if err := capabilityUnmarshal.Unmarshal(data, &typed); err != nil { + return nil, fmt.Errorf("decode request router descriptor: %w", err) + } + descriptor.RequestRouter = &typed + } if configSchema, ok := record.Metadata["config_schema"]; ok { schemas, err := decodeConfigSchemas(configSchema) if err != nil { @@ -128,6 +139,17 @@ func capabilityMetadata(descriptor *pluginv1.CapabilityDescriptor) (map[string]a } metadata["watch_sync_provider"] = value } + if descriptor.GetRequestRouter() != nil { + data, err := protojson.MarshalOptions{UseProtoNames: true}.Marshal(descriptor.GetRequestRouter()) + if err != nil { + return nil, fmt.Errorf("encode request router descriptor: %w", err) + } + var value map[string]any + if err := json.Unmarshal(data, &value); err != nil { + return nil, fmt.Errorf("decode request router descriptor JSON: %w", err) + } + metadata["request_router"] = value + } if len(descriptor.GetConfigSchema()) > 0 { schemas := make([]map[string]any, 0, len(descriptor.GetConfigSchema())) for _, schema := range descriptor.GetConfigSchema() { diff --git a/pkg/pluginsdk/convert/request_router_test.go b/pkg/pluginsdk/convert/request_router_test.go new file mode 100644 index 0000000..f1d4e6d --- /dev/null +++ b/pkg/pluginsdk/convert/request_router_test.go @@ -0,0 +1,61 @@ +package convert_test + +import ( + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/convert" +) + +func TestRequestRouterDescriptorRoundTrip(t *testing.T) { + manifest := &pluginv1.PluginManifest{Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "request_router.v1", + Id: "arr", + RequestRouter: &pluginv1.RequestRouterDescriptor{SupportsSeasons: true}, + }}} + records, err := convert.CapabilityRecordsFromManifest(manifest) + if err != nil { + t.Fatal(err) + } + decoded, err := convert.DecodeCapability(records[0]) + if err != nil { + t.Fatal(err) + } + if !decoded.GetRequestRouter().GetSupportsSeasons() { + t.Fatalf("decoded descriptor = %#v, want supports_seasons", decoded.GetRequestRouter()) + } +} + +// Metadata stored for a request router built on an older SDK has no +// request_router key; it must decode as declaring no season support. +func TestDecodeCapabilityWithoutRequestRouterDeclaresNoSeasonSupport(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "request_router.v1", + ID: "arr", + Metadata: map[string]any{"display_name": "Arr"}, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatal(err) + } + if decoded.GetRequestRouter() != nil { + t.Fatalf("decoded descriptor = %#v, want absent", decoded.GetRequestRouter()) + } +} + +func TestDecodeCapabilityDiscardsUnknownRequestRouterData(t *testing.T) { + record := convert.CapabilityRecord{ + Type: "request_router.v1", + ID: "arr", + Metadata: map[string]any{ + "request_router": map[string]any{"supports_seasons": true, "supports_future_thing": true}, + }, + } + decoded, err := convert.DecodeCapability(record) + if err != nil { + t.Fatalf("DecodeCapability() error = %v", err) + } + if !decoded.GetRequestRouter().GetSupportsSeasons() { + t.Fatalf("decoded descriptor = %#v, want supports_seasons", decoded.GetRequestRouter()) + } +} diff --git a/pkg/pluginsdk/manifest/manifest.go b/pkg/pluginsdk/manifest/manifest.go index 0da82bd..746eb43 100644 --- a/pkg/pluginsdk/manifest/manifest.go +++ b/pkg/pluginsdk/manifest/manifest.go @@ -116,6 +116,9 @@ func Validate(manifest *pluginv1.PluginManifest) error { if err := validateNetworkAccessCapability(capability); err != nil { return err } + if err := validateRequestRouterCapability(capability); err != nil { + return err + } } for _, schema := range manifest.GlobalConfigSchema { if err := validateConfigSchema(schema); err != nil { @@ -229,6 +232,15 @@ func validateNetworkAccessCapability(descriptor *pluginv1.CapabilityDescriptor) return nil } +// validateRequestRouterCapability keeps the descriptor optional so request +// routers built before it existed stay valid. +func validateRequestRouterCapability(descriptor *pluginv1.CapabilityDescriptor) error { + if descriptor.GetType() != capability.RequestRouter && descriptor.GetRequestRouter() != nil { + return fmt.Errorf("plugin capability %q: request_router descriptor requires type %q", descriptor.GetId(), capability.RequestRouter) + } + return nil +} + // ValidateCatalogPresentation applies the stricter presentation contract used // by curated catalogs. The general manifest validator keeps presentation // optional so older and directly uploaded plugins remain compatible. diff --git a/pkg/pluginsdk/manifest/request_router_test.go b/pkg/pluginsdk/manifest/request_router_test.go new file mode 100644 index 0000000..53ec6b8 --- /dev/null +++ b/pkg/pluginsdk/manifest/request_router_test.go @@ -0,0 +1,55 @@ +package manifest_test + +import ( + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + publicmanifest "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/manifest" +) + +func TestLoadRequestRouterDeclaresSeasonSupport(t *testing.T) { + raw := []byte(`{ + "plugin_id":"silo.requests-arr", "version":"1.0.0", "silo_api_version":"v1", + "capabilities":[{ + "type":"request_router.v1", "id":"arr", "display_name":"Arr", + "request_router":{"supports_seasons":true} + }] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + if !manifest.GetCapabilities()[0].GetRequestRouter().GetSupportsSeasons() { + t.Fatal("supports_seasons = false, want true") + } +} + +// A manifest written before the descriptor existed stays valid and declares no +// season support, so the host keeps today's whole-series behaviour. +func TestLoadRequestRouterWithoutDescriptorDeclaresNoSeasonSupport(t *testing.T) { + raw := []byte(`{ + "plugin_id":"silo.requests-arr", "version":"1.0.0", "silo_api_version":"v1", + "capabilities":[{"type":"request_router.v1", "id":"arr", "display_name":"Arr"}] + }`) + manifest, err := publicmanifest.Load(raw) + if err != nil { + t.Fatal(err) + } + capability := manifest.GetCapabilities()[0] + if capability.GetRequestRouter() != nil { + t.Fatalf("request router descriptor = %#v, want absent", capability.GetRequestRouter()) + } +} + +func TestValidateRequestRouterRejectsDescriptorOnOtherType(t *testing.T) { + manifest := &pluginv1.PluginManifest{ + PluginId: "silo.invalid", Version: "1.0.0", + Capabilities: []*pluginv1.CapabilityDescriptor{{ + Type: "scheduled_task.v1", Id: "task", + RequestRouter: &pluginv1.RequestRouterDescriptor{SupportsSeasons: true}, + }}, + } + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected request router descriptor on a scheduled task to fail") + } +} diff --git a/pkg/pluginsdk/runtime/request_router_test.go b/pkg/pluginsdk/runtime/request_router_test.go new file mode 100644 index 0000000..adca8aa --- /dev/null +++ b/pkg/pluginsdk/runtime/request_router_test.go @@ -0,0 +1,57 @@ +package runtime_test + +import ( + "context" + "net" + "slices" + "testing" + + pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1" + runtime "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginsdk/runtime" + "github.com/hashicorp/go-plugin" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" +) + +type recordingRequestRouter struct { + pluginv1.UnimplementedRequestRouterServer + seasons chan []int32 +} + +func (r recordingRequestRouter) Fulfill(_ context.Context, req *pluginv1.FulfillRequest) (*pluginv1.FulfillResponse, error) { + r.seasons <- req.GetRequest().GetSeasons() + return &pluginv1.FulfillResponse{}, nil +} + +func TestRequestRouterReceivesRequestedSeasons(t *testing.T) { + router := recordingRequestRouter{seasons: make(chan []int32, 1)} + plugins := runtime.DefaultPluginSet(runtime.CapabilityServers{Runtime: stubRuntime{}, RequestRouter: router}) + p := plugins[runtime.PluginSetName].(plugin.GRPCPlugin) + srv := grpc.NewServer() + if err := p.GRPCServer(nil, srv); err != nil { + t.Fatalf("GRPCServer = %v, want nil", err) + } + listener := bufconn.Listen(1 << 20) + go func() { _ = srv.Serve(listener) }() + t.Cleanup(srv.Stop) + + conn, err := grpc.NewClient("passthrough:///bufnet", + grpc.WithContextDialer(func(ctx context.Context, _ string) (net.Conn, error) { return listener.DialContext(ctx) }), + grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + t.Fatalf("NewClient = %v", err) + } + t.Cleanup(func() { _ = conn.Close() }) + + _, err = pluginv1.NewRequestRouterClient(conn).Fulfill(context.Background(), &pluginv1.FulfillRequest{ + CapabilityId: "arr", + Request: &pluginv1.RequestDescriptor{MediaType: "series", Title: "Example", Seasons: []int32{0, 4}}, + }) + if err != nil { + t.Fatalf("Fulfill = %v", err) + } + if got := <-router.seasons; !slices.Equal(got, []int32{0, 4}) { + t.Fatalf("seasons = %v, want [0 4]", got) + } +} diff --git a/proto/silo/plugin/v1/common.proto b/proto/silo/plugin/v1/common.proto index dae0ff9..2507ca6 100644 --- a/proto/silo/plugin/v1/common.proto +++ b/proto/silo/plugin/v1/common.proto @@ -6,6 +6,7 @@ option go_package = "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo import "google/protobuf/struct.proto"; import "silo/plugin/v1/network_access_provider.proto"; +import "silo/plugin/v1/request_router.proto"; import "silo/plugin/v1/watch_sync_provider.proto"; message SupportedPlatform { @@ -148,6 +149,11 @@ message CapabilityDescriptor { // of type "network_access_provider.v1". The host lists available overlay // providers from this descriptor without launching the plugin. NetworkAccessProviderDescriptor network_access_provider = 11; + + // Typed request-router contract metadata. Only meaningful for capabilities + // of type "request_router.v1". Optional: an absent descriptor declares no + // optional request-router features. + RequestRouterDescriptor request_router = 12; } // PluginPresentation describes a plugin for server operators and links them to diff --git a/proto/silo/plugin/v1/request_router.proto b/proto/silo/plugin/v1/request_router.proto index 309a707..ca23728 100644 --- a/proto/silo/plugin/v1/request_router.proto +++ b/proto/silo/plugin/v1/request_router.proto @@ -31,6 +31,27 @@ message RequestDescriptor { // on the downstream service (e.g. Seerr). Empty when the host cannot resolve it. string requester_email = 8; string requester_username = 9; + // seasons lists the requested season numbers of a series; 0 is Specials. + // Empty means the whole series, which is what every request meant before + // this field existed. Only set when media_type is "series". Plugins that + // declare RequestRouterDescriptor.supports_seasons must fulfil only these + // seasons; see that field for the full contract. + repeated int32 seasons = 10; +} + +// RequestRouterDescriptor is typed request_router.v1 contract metadata that the +// host reads from the manifest without launching the plugin. An absent +// descriptor means every flag is false, which is how the host treats plugins +// built before a flag existed. +message RequestRouterDescriptor { + // supports_seasons declares that Fulfill and CheckStatus honour + // RequestDescriptor.seasons. A declaring plugin acquires only the listed + // seasons; when the series already exists upstream it adds those seasons to + // what is already tracked and leaves other seasons unchanged. Repeating a + // request converges rather than duplicating work. The host sends a request + // for only the missing seasons of a series it already has only to plugins + // that set this flag, because other plugins would add the whole series. + bool supports_seasons = 1; } message RouterConnection {