import 'dart:async'; import 'package:grpc/grpc.dart'; import 'grpc_config.dart'; import '../generated/reflection.pbgrpc.dart'; import '../generated/descriptor.pb.dart'; /// Exception types for gRPC discovery operations. abstract class GrpcDiscoveryException implements Exception { final String message; GrpcDiscoveryException(this.message); @override String toString() => '$runtimeType: $message'; } /// Thrown when the gRPC reflection service returns an error. class ReflectionException extends GrpcDiscoveryException { final int errorCode; ReflectionException(super.message, this.errorCode); @override String toString() => 'ReflectionException: $message (code: $errorCode)'; } /// Thrown when connection to the server fails. class ConnectionException extends GrpcDiscoveryException { ConnectionException(super.message); } /// Represents a discovered gRPC service with its methods. class DiscoveredService { final String name; final List methods; const DiscoveredService({ required this.name, required this.methods, }); @override String toString() => 'DiscoveredService($name, methods: ${methods.length})'; } /// Represents a method within a discovered gRPC service. class DiscoveredMethod { final String name; final String inputType; final String outputType; final bool clientStreaming; final bool serverStreaming; const DiscoveredMethod({ required this.name, required this.inputType, required this.outputType, required this.clientStreaming, required this.serverStreaming, }); @override String toString() => 'DiscoveredMethod($name)'; } /// Client for discovering gRPC services using server reflection. /// /// Connects to a gRPC server's reflection service to enumerate available /// services and their method signatures. This is useful for development /// and proto generation. /// /// Example usage: /// ```dart /// final discovery = GrpcDiscoveryClient(config: GrpcConfig.development); /// try { /// final services = await discovery.listServices(); /// for (final service in services) { /// print('Service: $service'); /// } /// } finally { /// await discovery.close(); /// } /// ``` class GrpcDiscoveryClient { final GrpcConfig config; late final ClientChannel _channel; late final ServerReflectionClient _stub; bool _isInitialized = false; GrpcDiscoveryClient({required this.config}); /// Initializes the gRPC channel and reflection client. /// /// This is called automatically on first use, but can be called /// explicitly to verify connection. void _ensureInitialized() { if (_isInitialized) return; _channel = ClientChannel( config.host, port: config.port, options: ChannelOptions( credentials: config.useTls ? const ChannelCredentials.secure() : const ChannelCredentials.insecure(), connectionTimeout: config.timeout, ), ); _stub = ServerReflectionClient(_channel); _isInitialized = true; } /// Lists all available services on the server. /// /// Returns a list of fully qualified service names. /// Excludes the reflection service itself by default. /// /// Throws [ConnectionException] if connection fails. /// Throws [ReflectionException] if reflection service returns an error. Future> listServices({bool includeReflection = false}) async { _ensureInitialized(); try { final responseStream = _stub.serverReflectionInfo( _createRequestStream([ ServerReflectionRequest()..listServices = '', ]), ); final responses = await responseStream.toList(); if (responses.isEmpty) { throw ReflectionException('No response from reflection service', 0); } final response = responses.first; if (response.hasErrorResponse()) { throw ReflectionException( response.errorResponse.errorMessage, response.errorResponse.errorCode, ); } List services = response.listServicesResponse.service .map((s) => s.name) .toList(); if (!includeReflection) { services = services .where((s) => !s.contains('reflection')) .toList(); } return services; } on GrpcError catch (e) { throw ConnectionException('Failed to connect to ${config.address}: ${e.message}'); } } /// Gets file descriptors for a symbol (service, message, etc.). /// /// Returns a list of [FileDescriptorProto] that define the symbol /// and all its transitive dependencies. /// /// Throws [ConnectionException] if connection fails. /// Throws [ReflectionException] if symbol not found or other error. Future> getFileDescriptorsForSymbol( String symbol, ) async { _ensureInitialized(); try { final responseStream = _stub.serverReflectionInfo( _createRequestStream([ ServerReflectionRequest()..fileContainingSymbol = symbol, ]), ); final responses = await responseStream.toList(); if (responses.isEmpty) { throw ReflectionException('No response for symbol: $symbol', 0); } final response = responses.first; if (response.hasErrorResponse()) { throw ReflectionException( response.errorResponse.errorMessage, response.errorResponse.errorCode, ); } return response.fileDescriptorResponse.fileDescriptorProto .map((bytes) => FileDescriptorProto.fromBuffer(bytes)) .toList(); } on GrpcError catch (e) { throw ConnectionException('Failed to get descriptors: ${e.message}'); } } /// Gets file descriptors by filename. /// /// Returns a list of [FileDescriptorProto] for the specified file /// and its dependencies. /// /// Throws [ConnectionException] if connection fails. /// Throws [ReflectionException] if file not found or other error. Future> getFileDescriptorsByName( String filename, ) async { _ensureInitialized(); try { final responseStream = _stub.serverReflectionInfo( _createRequestStream([ ServerReflectionRequest()..fileByFilename = filename, ]), ); final responses = await responseStream.toList(); if (responses.isEmpty) { throw ReflectionException('No response for file: $filename', 0); } final response = responses.first; if (response.hasErrorResponse()) { throw ReflectionException( response.errorResponse.errorMessage, response.errorResponse.errorCode, ); } return response.fileDescriptorResponse.fileDescriptorProto .map((bytes) => FileDescriptorProto.fromBuffer(bytes)) .toList(); } on GrpcError catch (e) { throw ConnectionException('Failed to get file descriptor: ${e.message}'); } } /// Discovers all services and their methods. /// /// Returns a list of [DiscoveredService] containing service names /// and their method signatures. /// /// This is a convenience method that combines [listServices] and /// [getFileDescriptorsForSymbol] to provide detailed service information. Future> discoverAllServices() async { final serviceNames = await listServices(); final List discoveredServices = []; for (final serviceName in serviceNames) { try { final descriptors = await getFileDescriptorsForSymbol(serviceName); final methods = []; for (final descriptor in descriptors) { for (final service in descriptor.service) { // Match the service by fully qualified name final fullName = descriptor.package.isEmpty ? service.name : '${descriptor.package}.${service.name}'; if (fullName == serviceName) { for (final method in service.method) { methods.add(DiscoveredMethod( name: method.name, inputType: method.inputType, outputType: method.outputType, clientStreaming: method.clientStreaming, serverStreaming: method.serverStreaming, )); } } } } discoveredServices.add(DiscoveredService( name: serviceName, methods: methods, )); } on GrpcDiscoveryException { // If we can't get details for a service, add it with no methods discoveredServices.add(DiscoveredService( name: serviceName, methods: const [], )); } } return discoveredServices; } /// Discovers services matching a pattern. /// /// Filters discovered services by name containing the [pattern]. /// Case-insensitive matching. Future> discoverServicesMatching(String pattern) async { final services = await discoverAllServices(); final lowerPattern = pattern.toLowerCase(); return services .where((s) => s.name.toLowerCase().contains(lowerPattern)) .toList(); } Stream _createRequestStream( List requests, ) async* { for (final request in requests) { yield request; } } /// Closes the gRPC channel connection. /// /// Should be called when done with discovery to release resources. Future close() async { if (_isInitialized) { await _channel.shutdown(); _isInitialized = false; } } } /// Utility function to discover and print gRPC services. /// /// This is a convenience function for development and debugging. /// Connects to the server specified in [config], discovers all services, /// and prints them to the console. /// /// Returns a map of service names to their discovered service details. Future> discoverAndPrintServices({ GrpcConfig config = GrpcConfig.development, }) async { final discovery = GrpcDiscoveryClient(config: config); try { final services = await discovery.discoverAllServices(); final Map result = {}; for (final service in services) { result[service.name] = service; } return result; } finally { await discovery.close(); } }