All Downloads are FREE. Search and download functionalities are using the official Maven repository.

zio.kafka.admin.AdminClient.scala Maven / Gradle / Ivy

The newest version!
package zio.kafka.admin

import org.apache.kafka.clients.admin.ListOffsetsResult.{ ListOffsetsResultInfo => JListOffsetsResultInfo }
import org.apache.kafka.clients.admin.{
  Admin => JAdmin,
  AlterConfigOp => JAlterConfigOp,
  AlterConfigsOptions => JAlterConfigsOptions,
  AlterConsumerGroupOffsetsOptions => JAlterConsumerGroupOffsetsOptions,
  Config => JConfig,
  ConsumerGroupDescription => JConsumerGroupDescription,
  ConsumerGroupListing => JConsumerGroupListing,
  CreatePartitionsOptions => JCreatePartitionsOptions,
  CreateTopicsOptions => JCreateTopicsOptions,
  DeleteAclsOptions => _,
  DeleteConsumerGroupsOptions => JDeleteConsumerGroupsOptions,
  DeleteRecordsOptions => JDeleteRecordsOptions,
  DeleteTopicsOptions => JDeleteTopicsOptions,
  DescribeClusterOptions => JDescribeClusterOptions,
  DescribeConfigsOptions => JDescribeConfigsOptions,
  DescribeConsumerGroupsOptions => JDescribeConsumerGroupsOptions,
  DescribeTopicsOptions => JDescribeTopicsOptions,
  ListConsumerGroupOffsetsOptions => JListConsumerGroupOffsetsOptions,
  ListConsumerGroupOffsetsSpec => JListConsumerGroupOffsetsSpec,
  ListConsumerGroupsOptions => JListConsumerGroupsOptions,
  ListOffsetsOptions => JListOffsetsOptions,
  ListTopicsOptions => JListTopicsOptions,
  LogDirDescription => JLogDirDescription,
  MemberDescription => JMemberDescription,
  NewPartitions => JNewPartitions,
  NewTopic => JNewTopic,
  OffsetSpec => JOffsetSpec,
  ReplicaInfo => JReplicaInfo,
  TopicDescription => JTopicDescription,
  TopicListing => JTopicListing,
  _
}
import org.apache.kafka.clients.consumer.{ OffsetAndMetadata => JOffsetAndMetadata }
import org.apache.kafka.common.config.{ ConfigResource => JConfigResource }
import org.apache.kafka.common.errors.ApiException
import org.apache.kafka.common.{
  ConsumerGroupState => JConsumerGroupState,
  IsolationLevel => JIsolationLevel,
  KafkaFuture,
  Metric => JMetric,
  MetricName => JMetricName,
  Node => JNode,
  TopicPartition => JTopicPartition,
  TopicPartitionInfo => JTopicPartitionInfo,
  Uuid
}
import zio._
import zio.kafka.admin.acl._
import zio.kafka.utils.SslHelper

import java.util.Optional
import scala.annotation.{ nowarn, tailrec }
import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.jdk.CollectionConverters._
import scala.util.{ Failure, Success, Try }

trait AdminClient {

  import AdminClient._

  /**
   * Create multiple topics.
   */
  def createTopics(
    newTopics: Iterable[NewTopic],
    options: Option[CreateTopicsOptions] = None
  ): Task[Unit]

  /**
   * Create a single topic.
   */
  def createTopic(newTopic: NewTopic, validateOnly: Boolean = false): Task[Unit]

  /**
   * Delete consumer groups.
   */
  def deleteConsumerGroups(
    groupIds: Iterable[String],
    options: Option[DeleteConsumerGroupOptions] = None
  ): Task[Unit]

  /**
   * Delete multiple topics.
   */
  def deleteTopics(
    topics: Iterable[String],
    options: Option[DeleteTopicsOptions] = None
  ): Task[Unit]

  /**
   * Delete a single topic.
   */
  def deleteTopic(topic: String): Task[Unit]

  /**
   * Delete records.
   */
  def deleteRecords(
    recordsToDelete: Map[TopicPartition, RecordsToDelete],
    deleteRecordsOptions: Option[DeleteRecordsOptions] = None
  ): Task[Unit]

  /**
   * List the topics in the cluster.
   */
  def listTopics(listTopicsOptions: Option[ListTopicsOptions] = None): Task[Map[String, TopicListing]]

  /**
   * Describe the specified topics.
   */
  def describeTopics(
    topicNames: Iterable[String],
    options: Option[DescribeTopicsOptions] = None
  ): Task[Map[String, TopicDescription]]

  /**
   * Get the configuration for the specified resources.
   */
  def describeConfigs(
    configResources: Iterable[ConfigResource],
    options: Option[DescribeConfigsOptions] = None
  ): Task[Map[ConfigResource, KafkaConfig]]

  /**
   * Get the configuration for the specified resources async.
   */
  def describeConfigsAsync(
    configResources: Iterable[ConfigResource],
    options: Option[DescribeConfigsOptions] = None
  ): Task[Map[ConfigResource, Task[KafkaConfig]]]

  /**
   * Get the cluster nodes.
   */
  def describeClusterNodes(options: Option[DescribeClusterOptions] = None): Task[List[Node]]

  /**
   * Get the cluster controller.
   */
  def describeClusterController(options: Option[DescribeClusterOptions] = None): Task[Option[Node]]

  /**
   * Get the cluster id.
   */
  def describeClusterId(options: Option[DescribeClusterOptions] = None): Task[String]

  /**
   * Get the cluster authorized operations.
   */
  def describeClusterAuthorizedOperations(
    options: Option[DescribeClusterOptions] = None
  ): Task[Set[AclOperation]]

  /**
   * Add new partitions to a topic.
   */
  def createPartitions(
    newPartitions: Map[String, NewPartitions],
    options: Option[CreatePartitionsOptions] = None
  ): Task[Unit]

  /**
   * List offset for the specified partitions.
   */
  def listOffsets(
    topicPartitionOffsets: Map[TopicPartition, OffsetSpec],
    options: Option[ListOffsetsOptions] = None
  ): Task[Map[TopicPartition, ListOffsetsResultInfo]]

  /**
   * List offset for the specified partitions.
   */
  def listOffsetsAsync(
    topicPartitionOffsets: Map[TopicPartition, OffsetSpec],
    options: Option[ListOffsetsOptions] = None
  ): Task[Map[TopicPartition, Task[ListOffsetsResultInfo]]]

  /**
   * List Consumer Group offsets for the specified partitions.
   */
  def listConsumerGroupOffsets(
    groupId: String,
    options: Option[ListConsumerGroupOffsetsOptions] = None
  ): Task[Map[TopicPartition, OffsetAndMetadata]]

  /**
   * Given a mapping of consumer group IDs and list of partitions, list the consumer group offsets available in the
   * cluster for the specified consumer groups and partitions.
   */
  def listConsumerGroupOffsets(
    groupSpecs: Map[String, ListConsumerGroupOffsetsSpec]
  ): Task[Map[String, Map[TopicPartition, OffsetAndMetadata]]]

  /**
   * Given a mapping of consumer group IDs and list of partitions, list the consumer group offsets available in the
   * cluster for the specified consumer groups and partitions.
   */
  def listConsumerGroupOffsets(
    groupSpecs: Map[String, ListConsumerGroupOffsetsSpec],
    options: ListConsumerGroupOffsetsOptions
  ): Task[Map[String, Map[TopicPartition, OffsetAndMetadata]]]

  /**
   * Alter offsets for the specified partitions and consumer group.
   */
  def alterConsumerGroupOffsets(
    groupId: String,
    offsets: Map[TopicPartition, OffsetAndMetadata],
    options: Option[AlterConsumerGroupOffsetsOptions] = None
  ): Task[Unit]

  /**
   * Retrieves metrics for the underlying AdminClient
   */
  def metrics: Task[Map[MetricName, Metric]]

  /**
   * List the consumer groups in the cluster.
   */
  def listConsumerGroups(options: Option[ListConsumerGroupsOptions] = None): Task[List[ConsumerGroupListing]]

  /**
   * Describe the specified consumer groups.
   */
  def describeConsumerGroups(groupIds: String*): Task[Map[String, ConsumerGroupDescription]]

  /**
   * Describe the specified consumer groups.
   */
  def describeConsumerGroups(
    groupIds: List[String],
    options: Option[DescribeConsumerGroupsOptions]
  ): Task[Map[String, ConsumerGroupDescription]]

  /**
   * Remove the specified members from a consumer group.
   */
  def removeMembersFromConsumerGroup(groupId: String, membersToRemove: Set[String]): Task[Unit]

  /**
   * Remove all members from a consumer group.
   */
  def removeMembersFromConsumerGroup(groupId: String): Task[Unit]

  /**
   * Describe the log directories of the specified brokers
   */
  def describeLogDirs(
    brokersId: Iterable[Int]
  ): ZIO[Any, Throwable, Map[Int, Map[String, LogDirDescription]]]

  /**
   * Describe the log directories of the specified brokers async
   */
  def describeLogDirsAsync(
    brokersId: Iterable[Int]
  ): ZIO[Any, Throwable, Map[Int, Task[Map[String, LogDirDescription]]]]

  /**
   * Incrementally update the configuration for the specified resources. Only supported by brokers with version 2.3.0 or
   * higher. Use alterConfigs otherwise.
   */
  def incrementalAlterConfigs(
    configs: Map[ConfigResource, Iterable[AlterConfigOp]],
    options: AlterConfigsOptions
  ): Task[Unit]

  /**
   * Incrementally update the configuration for the specified resources async. Only supported by brokers with version
   * 2.3.0 or higher. Use alterConfigsAsync otherwise.
   */
  def incrementalAlterConfigsAsync(
    configs: Map[ConfigResource, Iterable[AlterConfigOp]],
    options: AlterConfigsOptions
  ): Task[Map[ConfigResource, Task[Unit]]]

  /**
   * Update the configuration for the specified resources.
   *
   * If you are using brokers with version 2.3.0 or higher, please use incrementalAlterConfigs instead.
   */
  def alterConfigs(configs: Map[ConfigResource, KafkaConfig], options: AlterConfigsOptions): Task[Unit]

  /**
   * Update the configuration for the specified resources async.
   *
   * If you are using brokers with version 2.3.0 or higher, please use incrementalAlterConfigs instead.
   */
  def alterConfigsAsync(
    configs: Map[ConfigResource, KafkaConfig],
    options: AlterConfigsOptions
  ): Task[Map[ConfigResource, Task[Unit]]]

  /*
   * Lists access control lists (ACLs) according to the supplied filter.
   *
   * Note: it may take some time for changes made by createAcls or deleteAcls to be reflected in the output of
   * describeAcls.
   */
  def describeAcls(filter: AclBindingFilter, options: Option[DescribeAclOptions] = None): Task[Set[AclBinding]]

  /**
   * Creates access control lists (ACLs) which are bound to specific resources.
   */
  def createAcls(acls: Set[AclBinding], options: Option[CreateAclOptions] = None): Task[Unit]

  /**
   * Creates access control lists (ACLs) which are bound to specific resources async.
   */
  def createAclsAsync(
    acls: Set[AclBinding],
    options: Option[CreateAclOptions] = None
  ): Task[Map[AclBinding, Task[Unit]]]

  /**
   * Deletes access control lists (ACLs) according to the supplied filters.
   */
  def deleteAcls(filters: Set[AclBindingFilter], options: Option[DeleteAclsOptions] = None): Task[Set[AclBinding]]

  /**
   * Deletes access control lists (ACLs) according to the supplied filters async.
   */
  def deleteAclsAsync(
    filters: Set[AclBindingFilter],
    options: Option[DeleteAclsOptions] = None
  ): Task[Map[AclBindingFilter, Task[Map[AclBinding, Option[Throwable]]]]]

}

object AdminClient {

  /**
   * Thin wrapper around apache java AdminClient. See java api for descriptions
   *
   * @param adminClient
   */
  private final class LiveAdminClient(
    private val adminClient: JAdmin
  ) extends AdminClient {

    /**
     * Create multiple topics.
     */
    override def createTopics(
      newTopics: Iterable[NewTopic],
      options: Option[CreateTopicsOptions] = None
    ): Task[Unit] = {
      val asJava = newTopics.map(_.asJava).asJavaCollection

      fromKafkaFutureVoid {
        ZIO.attempt(
          options
            .fold(adminClient.createTopics(asJava))(opts => adminClient.createTopics(asJava, opts.asJava))
            .all()
        )
      }
    }

    /**
     * Create a single topic.
     */
    override def createTopic(newTopic: NewTopic, validateOnly: Boolean = false): Task[Unit] =
      createTopics(List(newTopic), Some(CreateTopicsOptions(validateOnly = validateOnly, timeout = Option.empty)))

    /**
     * Delete consumer groups.
     */
    override def deleteConsumerGroups(
      groupIds: Iterable[String],
      options: Option[DeleteConsumerGroupOptions]
    ): Task[Unit] = {
      val asJava = groupIds.asJavaCollection
      fromKafkaFutureVoid {
        ZIO.attempt(
          options
            .fold(adminClient.deleteConsumerGroups(asJava))(opts =>
              adminClient.deleteConsumerGroups(asJava, opts.asJava)
            )
            .all()
        )
      }
    }

    /**
     * Delete multiple topics.
     */
    override def deleteTopics(
      topics: Iterable[String],
      options: Option[DeleteTopicsOptions] = None
    ): Task[Unit] = {
      val asJava = topics.asJavaCollection
      fromKafkaFutureVoid {
        ZIO.attempt(
          options
            .fold(adminClient.deleteTopics(asJava))(opts => adminClient.deleteTopics(asJava, opts.asJava))
            .all()
        )
      }
    }

    /**
     * Delete a single topic.
     */
    override def deleteTopic(topic: String): Task[Unit] =
      deleteTopics(List(topic))

    /**
     * Delete records.
     */
    override def deleteRecords(
      recordsToDelete: Map[TopicPartition, RecordsToDelete],
      deleteRecordsOptions: Option[DeleteRecordsOptions] = None
    ): Task[Unit] = {
      val records = recordsToDelete.map { case (k, v) => k.asJava -> v }.asJava
      fromKafkaFutureVoid {
        ZIO.attempt(
          deleteRecordsOptions
            .fold(adminClient.deleteRecords(records))(opts => adminClient.deleteRecords(records, opts.asJava))
            .all()
        )
      }
    }

    /**
     * List the topics in the cluster.
     */
    override def listTopics(listTopicsOptions: Option[ListTopicsOptions] = None): Task[Map[String, TopicListing]] =
      fromKafkaFuture {
        ZIO.attempt(
          listTopicsOptions
            .fold(adminClient.listTopics())(opts => adminClient.listTopics(opts.asJava))
            .namesToListings()
        )
      }.map(_.asScala.map { case (k, v) => k -> TopicListing(v) }.toMap)

    /**
     * Describe the specified topics.
     */
    override def describeTopics(
      topicNames: Iterable[String],
      options: Option[DescribeTopicsOptions] = None
    ): Task[Map[String, TopicDescription]] = {
      val asJava = topicNames.asJavaCollection
      fromKafkaFuture {
        ZIO.attempt(
          options
            .fold(adminClient.describeTopics(asJava))(opts => adminClient.describeTopics(asJava, opts.asJava))
            .allTopicNames()
        )
      }.flatMap { jTopicDescriptions =>
        ZIO.fromTry {
          jTopicDescriptions.asScala.toList.forEach { case (k, v) => AdminClient.TopicDescription(v).map(k -> _) }
            .map(_.toMap)
        }
      }
    }

    /**
     * Get the configuration for the specified resources.
     */
    override def describeConfigs(
      configResources: Iterable[ConfigResource],
      options: Option[DescribeConfigsOptions] = None
    ): Task[Map[ConfigResource, KafkaConfig]] = {
      val asJava = configResources.map(_.asJava).asJavaCollection
      fromKafkaFuture {
        ZIO.attempt(
          options
            .fold(adminClient.describeConfigs(asJava))(opts => adminClient.describeConfigs(asJava, opts.asJava))
            .all()
        )
      }.map {
        _.asScala.view.map { case (configResource, config) =>
          (ConfigResource(configResource), KafkaConfig(config))
        }.toMap
      }
    }

    /**
     * Get the configuration for the specified resources async.
     */
    override def describeConfigsAsync(
      configResources: Iterable[ConfigResource],
      options: Option[DescribeConfigsOptions] = None
    ): Task[Map[ConfigResource, Task[KafkaConfig]]] = {
      val asJava = configResources.map(_.asJava).asJavaCollection
      ZIO
        .attempt(
          options
            .fold(adminClient.describeConfigs(asJava))(opts => adminClient.describeConfigs(asJava, opts.asJava))
            .values()
        )
        .map {
          _.asScala.view.map { case (configResource, configFuture) =>
            (
              ConfigResource(configResource),
              ZIO
                .fromCompletionStage(configFuture.toCompletionStage)
                .map(config => KafkaConfig(config))
            )

          }.toMap
        }
    }

    private def describeCluster(options: Option[DescribeClusterOptions]): Task[DescribeClusterResult] =
      ZIO.attempt(
        options.fold(adminClient.describeCluster())(opts => adminClient.describeCluster(opts.asJava))
      )

    /**
     * Get the cluster nodes.
     */
    override def describeClusterNodes(options: Option[DescribeClusterOptions] = None): Task[List[Node]] =
      fromKafkaFuture(
        describeCluster(options).map(_.nodes())
      ).flatMap { nodes =>
        ZIO.fromTry {
          nodes.asScala.toList.forEach { jNode =>
            Node(jNode) match {
              case Some(node) => Success(node)
              case None       => Failure(new RuntimeException("NoNode not expected when listing cluster nodes"))
            }
          }
        }
      }

    /**
     * Get the cluster controller.
     */
    override def describeClusterController(options: Option[DescribeClusterOptions] = None): Task[Option[Node]] =
      fromKafkaFuture(
        describeCluster(options).map(_.controller())
      ).map(Node(_))

    /**
     * Get the cluster id.
     */
    override def describeClusterId(options: Option[DescribeClusterOptions] = None): Task[String] =
      fromKafkaFuture(
        describeCluster(options).map(_.clusterId())
      )

    /**
     * Get the cluster authorized operations.
     */
    override def describeClusterAuthorizedOperations(
      options: Option[DescribeClusterOptions] = None
    ): Task[Set[AclOperation]] =
      for {
        res <- describeCluster(options)
        opt <- fromKafkaFuture(ZIO.attempt(res.authorizedOperations())).map(Option(_))
        lst <- ZIO.fromOption(opt.map(_.asScala.toSet)).orElseSucceed(Set.empty)
        aclOperations = lst.map(AclOperation.apply)
      } yield aclOperations

    /**
     * Add new partitions to a topic.
     */
    override def createPartitions(
      newPartitions: Map[String, NewPartitions],
      options: Option[CreatePartitionsOptions] = None
    ): Task[Unit] = {
      val asJava = newPartitions.map { case (k, v) => k -> v.asJava }.asJava
      fromKafkaFutureVoid {
        ZIO.attempt(
          options
            .fold(adminClient.createPartitions(asJava))(opts => adminClient.createPartitions(asJava, opts.asJava))
            .all()
        )
      }
    }

    /**
     * List offset for the specified partitions.
     */
    override def listOffsets(
      topicPartitionOffsets: Map[TopicPartition, OffsetSpec],
      options: Option[ListOffsetsOptions] = None
    ): Task[Map[TopicPartition, ListOffsetsResultInfo]] = {
      val asJava = topicPartitionOffsets.bimap(_.asJava, _.asJava).asJava
      fromKafkaFuture {
        ZIO.attempt(
          options
            .fold(adminClient.listOffsets(asJava))(opts => adminClient.listOffsets(asJava, opts.asJava))
            .all()
        )
      }
    }.map(_.asScala.bimap(TopicPartition(_), ListOffsetsResultInfo(_)).toMap)

    /**
     * List offset for the specified partitions.
     */
    override def listOffsetsAsync(
      topicPartitionOffsets: Map[TopicPartition, OffsetSpec],
      options: Option[ListOffsetsOptions]
    ): Task[Map[TopicPartition, Task[ListOffsetsResultInfo]]] = {
      val topicPartitionOffsetsAsJava = topicPartitionOffsets.bimap(_.asJava, _.asJava)
      val topicPartitionsAsJava       = topicPartitionOffsetsAsJava.keySet
      val asJava                      = topicPartitionOffsetsAsJava.asJava
      ZIO.attempt {
        val listOffsetsResult = options
          .fold(adminClient.listOffsets(asJava))(opts => adminClient.listOffsets(asJava, opts.asJava))
        topicPartitionsAsJava.map(tp => tp -> listOffsetsResult.partitionResult(tp))
      }
    }.map {
      _.view.map { case (topicPartition, listOffsetResultInfoFuture) =>
        (
          TopicPartition(topicPartition),
          ZIO.fromCompletionStage(listOffsetResultInfoFuture.toCompletionStage).map(ListOffsetsResultInfo(_))
        )
      }.toMap
    }

    /**
     * List Consumer Group offsets for the specified partitions.
     */
    override def listConsumerGroupOffsets(
      groupId: String,
      options: Option[ListConsumerGroupOffsetsOptions] = None
    ): Task[Map[TopicPartition, OffsetAndMetadata]] =
      fromKafkaFuture {
        ZIO.attempt(
          options
            .fold(adminClient.listConsumerGroupOffsets(groupId))(opts =>
              adminClient.listConsumerGroupOffsets(groupId, opts.asJava)
            )
            .partitionsToOffsetAndMetadata()
        )
      }
        .map(_.asScala.filter { case (_, om) => om ne null }.bimap(TopicPartition(_), OffsetAndMetadata(_)).toMap)

    /**
     * List the consumer group offsets available in the cluster for the specified consumer groups.
     */
    override def listConsumerGroupOffsets(
      groupSpecs: Map[String, ListConsumerGroupOffsetsSpec]
    ): Task[Map[String, Map[TopicPartition, OffsetAndMetadata]]] =
      fromKafkaFuture {
        ZIO.attempt(
          adminClient
            .listConsumerGroupOffsets(groupSpecs.map { case (groupId, offsetsSpec) =>
              (groupId, offsetsSpec.asJava)
            }.asJava)
            .all()
        )
      }.map {
        _.asScala.map { case (groupId, offsets) =>
          groupId ->
            offsets.asScala.filter { case (_, om) => om ne null }
              .bimap(TopicPartition(_), OffsetAndMetadata(_))
              .toMap
        }.toMap
      }

    override def listConsumerGroupOffsets(
      groupSpecs: Map[String, ListConsumerGroupOffsetsSpec],
      options: ListConsumerGroupOffsetsOptions
    ): Task[Map[String, Map[TopicPartition, OffsetAndMetadata]]] =
      fromKafkaFuture {
        ZIO.attempt(
          adminClient
            .listConsumerGroupOffsets(
              groupSpecs.map { case (groupId, offsetsSpec) => (groupId, offsetsSpec.asJava) }.asJava,
              options.asJava
            )
            .all()
        )
      }.map {
        _.asScala.map { case (groupId, offsets) =>
          groupId ->
            offsets.asScala.filter { case (_, om) => om ne null }
              .bimap(TopicPartition(_), OffsetAndMetadata(_))
              .toMap
        }.toMap
      }

    /**
     * Alter offsets for the specified partitions and consumer group.
     */
    override def alterConsumerGroupOffsets(
      groupId: String,
      offsets: Map[TopicPartition, OffsetAndMetadata],
      options: Option[AlterConsumerGroupOffsetsOptions] = None
    ): Task[Unit] = {
      val asJava = offsets.bimap(_.asJava, _.asJava).asJava
      fromKafkaFutureVoid {
        ZIO.attempt(
          options
            .fold(adminClient.alterConsumerGroupOffsets(groupId, asJava))(opts =>
              adminClient.alterConsumerGroupOffsets(groupId, asJava, opts.asJava)
            )
            .all()
        )
      }
    }

    /**
     * Retrieves metrics for the underlying AdminClient
     */
    override def metrics: Task[Map[MetricName, Metric]] =
      ZIO.attempt(
        adminClient.metrics().asScala.toMap.map { case (metricName, metric) =>
          (MetricName(metricName), Metric(metric))
        }
      )

    /**
     * List the consumer groups in the cluster.
     */
    override def listConsumerGroups(
      options: Option[ListConsumerGroupsOptions] = None
    ): Task[List[ConsumerGroupListing]] =
      fromKafkaFuture {
        ZIO.attempt(
          options
            .fold(adminClient.listConsumerGroups())(opts => adminClient.listConsumerGroups(opts.asJava))
            .all()
        )
      }.map(_.asScala.map(ConsumerGroupListing(_)).toList)

    /**
     * Describe the specified consumer groups.
     */
    override def describeConsumerGroups(groupIds: String*): Task[Map[String, ConsumerGroupDescription]] =
      describeConsumerGroups(groupIds.toList, options = None)

    /**
     * Describe the specified consumer groups.
     */
    override def describeConsumerGroups(
      groupIds: List[String],
      options: Option[DescribeConsumerGroupsOptions]
    ): Task[Map[String, ConsumerGroupDescription]] =
      fromKafkaFuture(
        ZIO.attempt(
          options
            .fold(adminClient.describeConsumerGroups(groupIds.asJavaCollection))(opts =>
              adminClient.describeConsumerGroups(groupIds.asJavaCollection, opts.asJava)
            )
            .all
        )
      ).map(_.asScala.map { case (k, v) => k -> ConsumerGroupDescription(v) }.toMap)

    /**
     * Remove the specified members from a consumer group.
     */
    override def removeMembersFromConsumerGroup(groupId: String, membersToRemove: Set[String]): Task[Unit] = {
      val options = new RemoveMembersFromConsumerGroupOptions(
        membersToRemove.map(new MemberToRemove(_)).asJavaCollection
      )
      fromKafkaFuture(
        ZIO.attempt(
          adminClient.removeMembersFromConsumerGroup(groupId, options).all()
        )
      ).unit
    }

    /**
     * Remove all members from a consumer group.
     */
    override def removeMembersFromConsumerGroup(groupId: String): Task[Unit] = {
      val options = new RemoveMembersFromConsumerGroupOptions()
      fromKafkaFuture(
        ZIO.attempt(
          adminClient.removeMembersFromConsumerGroup(groupId, options).all()
        )
      ).unit
    }

    override def describeLogDirs(
      brokersId: Iterable[Int]
    ): ZIO[Any, Throwable, Map[Int, Map[String, LogDirDescription]]] =
      fromKafkaFuture(
        ZIO.attempt(
          adminClient.describeLogDirs(brokersId.map(Int.box).asJavaCollection).allDescriptions()
        )
      ).map {
        _.asScala.bimap(_.intValue, _.asScala.bimap(identity, LogDirDescription(_)).toMap).toMap
      }

    /**
     * Describe the log directories of the specified brokers async
     */
    override def describeLogDirsAsync(
      brokersId: Iterable[Int]
    ): ZIO[Any, Throwable, Map[Int, Task[Map[String, LogDirDescription]]]] =
      ZIO
        .attempt(
          adminClient.describeLogDirs(brokersId.map(Int.box).asJavaCollection).descriptions()
        )
        .map {
          _.asScala.view.map { case (brokerId, descriptionsFuture) =>
            (
              brokerId.intValue(),
              ZIO
                .fromCompletionStage(descriptionsFuture.toCompletionStage)
                .map(_.asScala.toMap.map { case (k, v) => (k, LogDirDescription(v)) })
            )
          }.toMap
        }

    override def incrementalAlterConfigs(
      configs: Map[ConfigResource, Iterable[AlterConfigOp]],
      options: AlterConfigsOptions
    ): Task[Unit] =
      fromKafkaFutureVoid(
        ZIO
          .attempt(
            adminClient
              .incrementalAlterConfigs(
                configs.map { case (configResource, alterConfigOps) =>
                  (configResource.asJava, alterConfigOps.map(_.asJava).asJavaCollection)
                }.asJava,
                options.asJava
              )
              .all()
          )
      )

    override def incrementalAlterConfigsAsync(
      configs: Map[ConfigResource, Iterable[AlterConfigOp]],
      options: AlterConfigsOptions
    ): Task[Map[ConfigResource, Task[Unit]]] =
      ZIO
        .attempt(
          adminClient
            .incrementalAlterConfigs(
              configs.map { case (configResource, alterConfigOps) =>
                (configResource.asJava, alterConfigOps.map(_.asJava).asJavaCollection)
              }.asJava,
              options.asJava
            )
            .values()
        )
        .map(_.asScala.map { case (configResource, kf) =>
          (ConfigResource(configResource), ZIO.fromCompletionStage(kf.toCompletionStage).unit)
        }.toMap)

    @nowarn("msg=deprecated")
    override def alterConfigs(configs: Map[ConfigResource, KafkaConfig], options: AlterConfigsOptions): Task[Unit] =
      fromKafkaFutureVoid(
        ZIO
          .attempt(
            adminClient
              .alterConfigs(
                configs.map { case (configResource, kafkaConfig) =>
                  (configResource.asJava, kafkaConfig.asJava)
                }.asJava,
                options.asJava
              )
              .all()
          )
      )

    @nowarn("msg=deprecated")
    override def alterConfigsAsync(
      configs: Map[ConfigResource, KafkaConfig],
      options: AlterConfigsOptions
    ): Task[Map[ConfigResource, Task[Unit]]] =
      ZIO
        .attempt(
          adminClient
            .alterConfigs(
              configs.map { case (configResource, kafkaConfig) =>
                (configResource.asJava, kafkaConfig.asJava)
              }.asJava,
              options.asJava
            )
            .values()
        )
        .map(_.asScala.map { case (configResource, kf) =>
          (ConfigResource(configResource), ZIO.fromCompletionStage(kf.toCompletionStage).unit)
        }.toMap)

    override def describeAcls(
      filter: AclBindingFilter,
      options: Option[DescribeAclOptions]
    ): Task[Set[AclBinding]] =
      fromKafkaFuture(
        ZIO
          .attempt(
            options
              .fold(adminClient.describeAcls(filter.asJava))(opt => adminClient.describeAcls(filter.asJava, opt.asJava))
              .values()
          )
      ).map(_.asScala.view.map(AclBinding(_)).toSet)

    override def createAcls(acls: Set[AclBinding], options: Option[CreateAclOptions]): Task[Unit] =
      fromKafkaFutureVoid(
        ZIO
          .attempt(
            options
              .fold(adminClient.createAcls(acls.map(_.asJava).asJava))(opt =>
                adminClient.createAcls(acls.map(_.asJava).asJava, opt.asJava)
              )
              .all()
          )
      )

    override def createAclsAsync(
      acls: Set[AclBinding],
      options: Option[CreateAclOptions]
    ): Task[Map[AclBinding, Task[Unit]]] =
      ZIO
        .attempt(
          options
            .fold(adminClient.createAcls(acls.map(_.asJava).asJava))(opt =>
              adminClient.createAcls(acls.map(_.asJava).asJava, opt.asJava)
            )
            .values()
        )
        .map(_.asScala.view.map { case (k, v) =>
          (AclBinding(k), ZIO.fromCompletionStage(v.toCompletionStage).unit)
        }.toMap)

    override def deleteAcls(filters: Set[AclBindingFilter], options: Option[DeleteAclsOptions]): Task[Set[AclBinding]] =
      fromKafkaFuture(
        ZIO
          .attempt(
            options
              .fold(adminClient.deleteAcls(filters.map(_.asJava).asJava))(opt =>
                adminClient.deleteAcls(filters.map(_.asJava).asJava, opt.asJava)
              )
              .all()
          )
      ).map(_.asScala.view.map(AclBinding(_)).toSet)

    override def deleteAclsAsync(
      filters: Set[AclBindingFilter],
      options: Option[DeleteAclsOptions]
    ): Task[Map[AclBindingFilter, Task[Map[AclBinding, Option[Throwable]]]]] =
      ZIO
        .attempt(
          options
            .fold(adminClient.deleteAcls(filters.map(_.asJava).asJava))(opt =>
              adminClient.deleteAcls(filters.map(_.asJava).asJava, opt.asJava)
            )
            .values()
        )
        .map(_.asScala.view.map { case (k, v) =>
          (
            AclBindingFilter(k),
            ZIO
              .fromCompletionStage(v.toCompletionStage)
              .map(
                _.values().asScala.view.map { filterRes =>
                  // FilterResult.binding() is claimed to be nullable but in fact it is not: see DeleteAclsResponse.aclBinding
                  AclBinding(filterRes.binding()) -> Option(filterRes.exception())
                }.toMap
              )
          )
        }.toMap)

  }

  val live: ZLayer[AdminClientSettings, Throwable, AdminClient] =
    ZLayer.scoped {
      for {
        settings <- ZIO.service[AdminClientSettings]
        admin    <- make(settings)
      } yield admin
    }

  def fromKafkaFuture[R, T](kfv: RIO[R, KafkaFuture[T]]): RIO[R, T] =
    kfv.flatMap(f => ZIO.fromCompletionStage(f.toCompletionStage))

  def fromKafkaFutureVoid[R](kfv: RIO[R, KafkaFuture[Void]]): RIO[R, Unit] =
    fromKafkaFuture(kfv).unit

  final case class ConfigResource(`type`: ConfigResourceType, name: String) {
    lazy val asJava: JConfigResource = new JConfigResource(`type`.asJava, name)
  }

  object ConfigResource {
    def apply(jcr: JConfigResource): ConfigResource =
      ConfigResource(`type` = ConfigResourceType(jcrt = jcr.`type`()), name = jcr.name())
  }

  trait ConfigResourceType {
    def asJava: JConfigResource.Type
  }

  object ConfigResourceType {
    case object BrokerLogger extends ConfigResourceType {
      override def asJava = JConfigResource.Type.BROKER_LOGGER
    }

    case object Broker extends ConfigResourceType {
      override def asJava = JConfigResource.Type.BROKER
    }

    case object Topic extends ConfigResourceType {
      override def asJava = JConfigResource.Type.TOPIC
    }

    case object Unknown extends ConfigResourceType {
      override def asJava = JConfigResource.Type.UNKNOWN
    }

    case object ClientMetrics extends ConfigResourceType {
      override def asJava = JConfigResource.Type.CLIENT_METRICS
    }

    def apply(jcrt: JConfigResource.Type): ConfigResourceType =
      jcrt match {
        case JConfigResource.Type.BROKER_LOGGER  => BrokerLogger
        case JConfigResource.Type.BROKER         => Broker
        case JConfigResource.Type.TOPIC          => Topic
        case JConfigResource.Type.UNKNOWN        => Unknown
        case JConfigResource.Type.CLIENT_METRICS => ClientMetrics
      }
  }

  sealed trait ConsumerGroupState {
    def asJava: JConsumerGroupState
  }

  object ConsumerGroupState {
    case object Unknown extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.UNKNOWN
    }

    case object PreparingRebalance extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.PREPARING_REBALANCE
    }

    case object CompletingRebalance extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.COMPLETING_REBALANCE
    }

    case object Stable extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.STABLE
    }

    case object Dead extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.DEAD
    }

    case object Empty extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.EMPTY
    }

    case object Assigning extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.ASSIGNING
    }

    case object Reconciling extends ConsumerGroupState {
      override def asJava: JConsumerGroupState = JConsumerGroupState.RECONCILING
    }

    def apply(state: JConsumerGroupState): ConsumerGroupState =
      state match {
        case JConsumerGroupState.UNKNOWN              => ConsumerGroupState.Unknown
        case JConsumerGroupState.PREPARING_REBALANCE  => ConsumerGroupState.PreparingRebalance
        case JConsumerGroupState.COMPLETING_REBALANCE => ConsumerGroupState.CompletingRebalance
        case JConsumerGroupState.STABLE               => ConsumerGroupState.Stable
        case JConsumerGroupState.DEAD                 => ConsumerGroupState.Dead
        case JConsumerGroupState.EMPTY                => ConsumerGroupState.Empty
        case JConsumerGroupState.ASSIGNING            => ConsumerGroupState.Assigning
        case JConsumerGroupState.RECONCILING          => ConsumerGroupState.Reconciling
      }
  }

  final case class MemberDescription(
    consumerId: String,
    groupInstanceId: Option[String],
    clientId: String,
    host: String,
    assignment: Set[TopicPartition]
  )

  object MemberDescription {
    def apply(desc: JMemberDescription): MemberDescription =
      MemberDescription(
        consumerId = desc.consumerId,
        groupInstanceId = desc.groupInstanceId.toScala,
        clientId = desc.clientId(),
        host = desc.host(),
        assignment = desc.assignment.topicPartitions().asScala.map(TopicPartition.apply).toSet
      )
  }

  final case class ConsumerGroupDescription(
    groupId: String,
    isSimpleConsumerGroup: Boolean,
    members: List[MemberDescription],
    partitionAssignor: String,
    state: ConsumerGroupState,
    coordinator: Option[Node],
    authorizedOperations: Set[AclOperation]
  )

  object ConsumerGroupDescription {

    def apply(description: JConsumerGroupDescription): ConsumerGroupDescription =
      ConsumerGroupDescription(
        groupId = description.groupId,
        isSimpleConsumerGroup = description.isSimpleConsumerGroup,
        members = description.members.asScala.map(MemberDescription.apply).toList,
        partitionAssignor = description.partitionAssignor,
        state = ConsumerGroupState(description.state),
        coordinator = Node(description.coordinator()),
        authorizedOperations = Option(description.authorizedOperations())
          .fold(Set.empty[AclOperation])(_.asScala.map(AclOperation.apply).toSet)
      )
  }

  final case class CreatePartitionsOptions(
    validateOnly: Boolean = false,
    retryOnQuotaViolation: Boolean = true,
    timeout: Option[Duration]
  ) {
    def asJava: JCreatePartitionsOptions = {
      val opts = new JCreatePartitionsOptions()
        .validateOnly(validateOnly)
        .retryOnQuotaViolation(retryOnQuotaViolation)

      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class CreateTopicsOptions(validateOnly: Boolean, timeout: Option[Duration]) {
    def asJava: JCreateTopicsOptions = {
      val opts = new JCreateTopicsOptions().validateOnly(validateOnly)
      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class DeleteConsumerGroupOptions(timeout: Option[Duration]) {
    def asJava: JDeleteConsumerGroupsOptions = {
      val opts = new JDeleteConsumerGroupsOptions()
      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class DeleteTopicsOptions(retryOnQuotaViolation: Boolean = true, timeout: Option[Duration]) {
    def asJava: JDeleteTopicsOptions = {
      val opts = new JDeleteTopicsOptions().retryOnQuotaViolation(retryOnQuotaViolation)
      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class ListTopicsOptions(listInternal: Boolean = false, timeout: Option[Duration]) {
    def asJava: JListTopicsOptions = {
      val opts = new JListTopicsOptions().listInternal(listInternal)
      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class DescribeTopicsOptions(includeAuthorizedOperations: Boolean, timeout: Option[Duration]) {
    def asJava: JDescribeTopicsOptions = {
      val opts = new JDescribeTopicsOptions().includeAuthorizedOperations(includeAuthorizedOperations)
      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class DescribeConfigsOptions(
    includeSynonyms: Boolean = false,
    includeDocumentation: Boolean = false,
    timeout: Option[Duration]
  ) {
    def asJava: JDescribeConfigsOptions = {
      val opts = new JDescribeConfigsOptions()
        .includeSynonyms(includeSynonyms)
        .includeDocumentation(includeDocumentation)

      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class DescribeClusterOptions(includeAuthorizedOperations: Boolean, timeout: Option[Duration]) {
    lazy val asJava: JDescribeClusterOptions = {
      val opts = new JDescribeClusterOptions().includeAuthorizedOperations(includeAuthorizedOperations)
      timeout.fold(opts)(timeout => opts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class DescribeConsumerGroupsOptions(includeAuthorizedOperations: Boolean, timeout: Option[Duration]) {
    lazy val asJava: JDescribeConsumerGroupsOptions = {
      val jOpts = new JDescribeConsumerGroupsOptions()
        .includeAuthorizedOperations(includeAuthorizedOperations)
      timeout.fold(jOpts)(timeout => jOpts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class AlterConfigsOptions(validateOnly: Boolean = false, timeout: Option[Duration] = None) {
    lazy val asJava: JAlterConfigsOptions = {
      val jOpts = new JAlterConfigsOptions().validateOnly(validateOnly)
      timeout.fold(jOpts)(timeout => jOpts.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class AlterConfigOp(configEntry: ConfigEntry, opType: AlterConfigOpType) {
    lazy val asJava: JAlterConfigOp = new JAlterConfigOp(configEntry, opType.asJava)
  }

  sealed trait AlterConfigOpType {
    def asJava: JAlterConfigOp.OpType
  }

  object AlterConfigOpType {
    case object Set extends AlterConfigOpType {
      override def asJava = JAlterConfigOp.OpType.SET
    }

    case object Delete extends AlterConfigOpType {
      override def asJava = JAlterConfigOp.OpType.DELETE
    }

    case object Append extends AlterConfigOpType {
      override def asJava = JAlterConfigOp.OpType.APPEND
    }

    case object Substract extends AlterConfigOpType {
      override def asJava = JAlterConfigOp.OpType.SUBTRACT
    }
  }

  final case class MetricName(name: String, group: String, description: String, tags: Map[String, String])
  object MetricName {
    def apply(jmn: JMetricName): MetricName =
      MetricName(
        name = jmn.name(),
        group = jmn.group(),
        description = jmn.description(),
        tags = jmn.tags().asScala.toMap
      )
  }

  final case class Metric(name: MetricName, metricValue: AnyRef)
  object Metric {
    def apply(jm: JMetric): Metric = Metric(name = MetricName(jmn = jm.metricName()), metricValue = jm.metricValue())
  }

  final case class NewTopic(
    name: String,
    numPartitions: Int,
    replicationFactor: Short,
    configs: Map[String, String] = Map.empty
  ) {
    def asJava: JNewTopic = {
      val jn = new JNewTopic(name, numPartitions, replicationFactor)

      if (configs.nonEmpty) {
        val _ = jn.configs(configs.asJava)
      }

      jn
    }
  }

  final case class NewPartitions(
    totalCount: Int,
    newAssignments: List[List[Int]] = List.empty
  ) {
    def asJava: JNewPartitions =
      if (newAssignments.nonEmpty)
        JNewPartitions.increaseTo(totalCount, newAssignments.map(_.map(Int.box).asJava).asJava)
      else JNewPartitions.increaseTo(totalCount)
  }

  /**
   * @param id
   *   >= 0
   * @param host
   *   can't be empty string if present
   * @param port
   *   can't be negative if present
   */
  final case class Node(id: Int, host: Option[String], port: Option[Int], rack: Option[String] = None) {
    lazy val asJava: JNode = new JNode(id, host.getOrElse(""), port.getOrElse(-1), rack.orNull)
  }
  object Node {
    def apply(jNode: JNode): Option[Node] =
      Option(jNode)
        .filter(_.id() >= 0)
        .map { jNode =>
          Node(
            id = jNode.id(),
            host = Option(jNode.host()).filterNot(_.isEmpty),
            port = Option(jNode.port()).filter(_ >= 0),
            rack = Option(jNode.rack())
          )
        }
  }

  final case class TopicDescription(
    name: String,
    internal: Boolean,
    partitions: List[TopicPartitionInfo],
    authorizedOperations: Option[Set[AclOperation]]
  )

  object TopicDescription {
    def apply(jt: JTopicDescription): Try[TopicDescription] = {
      val authorizedOperations = Option(jt.authorizedOperations).map(_.asScala.toSet).map(_.map(AclOperation.apply))

      jt.partitions.asScala.toList.forEach(TopicPartitionInfo.apply).map { partitions =>
        TopicDescription(
          name = jt.name,
          internal = jt.isInternal,
          partitions = partitions,
          authorizedOperations = authorizedOperations
        )
      }
    }
  }

  final case class TopicPartitionInfo(partition: Int, leader: Option[Node], replicas: List[Node], isr: List[Node]) {
    lazy val asJava: JTopicPartitionInfo =
      new JTopicPartitionInfo(
        partition,
        leader.map(_.asJava).getOrElse(JNode.noNode()),
        replicas.map(_.asJava).asJava,
        isr.map(_.asJava).asJava
      )
  }

  object TopicPartitionInfo {
    def apply(jtpi: JTopicPartitionInfo): Try[TopicPartitionInfo] = {
      val replicas: Try[List[Node]] =
        jtpi
          .replicas()
          .asScala
          .toList
          .forEach { jNode =>
            Node(jNode) match {
              case Some(node) => Success(node)
              case None       => Failure(new RuntimeException("NoNode node not expected among topic replicas"))
            }
          }

      val inSyncReplicas: Try[List[Node]] =
        jtpi
          .isr()
          .asScala
          .toList
          .forEach { jNode =>
            Node(jNode) match {
              case Some(node) => Success(node)
              case None       => Failure(new RuntimeException("NoNode node not expected among topic in sync replicas"))
            }
          }

      for {
        replicas       <- replicas
        inSyncReplicas <- inSyncReplicas
      } yield TopicPartitionInfo(
        partition = jtpi.partition(),
        leader = Node(jtpi.leader()),
        replicas = replicas,
        isr = inSyncReplicas
      )
    }
  }

  final case class TopicListing(name: String, topicId: Uuid, isInternal: Boolean) {
    def asJava: JTopicListing = new JTopicListing(name, topicId, isInternal)
  }

  object TopicListing {
    def apply(jtl: JTopicListing): TopicListing = TopicListing(jtl.name(), jtl.topicId(), jtl.isInternal)
  }

  final case class TopicPartition(
    name: String,
    partition: Int
  ) {
    def asJava: JTopicPartition = new JTopicPartition(name, partition)
  }

  object TopicPartition {
    def apply(tp: JTopicPartition): TopicPartition = new TopicPartition(name = tp.topic(), partition = tp.partition())
  }

  sealed abstract class OffsetSpec {
    def asJava: JOffsetSpec
  }

  object OffsetSpec {
    case object EarliestSpec extends OffsetSpec {
      override def asJava: JOffsetSpec = JOffsetSpec.earliest()
    }

    case object LatestSpec extends OffsetSpec {
      override def asJava: JOffsetSpec = JOffsetSpec.latest()
    }

    final case class TimestampSpec(timestamp: Long) extends OffsetSpec {
      override def asJava: JOffsetSpec = JOffsetSpec.forTimestamp(timestamp)
    }
  }

  sealed abstract class IsolationLevel {
    def asJava: JIsolationLevel
  }

  object IsolationLevel {
    case object ReadUncommitted extends IsolationLevel {
      override def asJava: JIsolationLevel = JIsolationLevel.READ_UNCOMMITTED
    }

    case object ReadCommitted extends IsolationLevel {
      override def asJava: JIsolationLevel = JIsolationLevel.READ_COMMITTED
    }
  }

  final case class DeleteRecordsOptions(timeout: Option[Duration]) {
    def asJava: JDeleteRecordsOptions = {
      val deleteRecordsOpt = new JDeleteRecordsOptions()
      timeout.fold(deleteRecordsOpt)(timeout => deleteRecordsOpt.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class ListOffsetsOptions(
    isolationLevel: IsolationLevel = IsolationLevel.ReadUncommitted,
    timeout: Option[Duration]
  ) {
    def asJava: JListOffsetsOptions = {
      val offsetOpt = new JListOffsetsOptions(isolationLevel.asJava)
      timeout.fold(offsetOpt)(timeout => offsetOpt.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class ListOffsetsResultInfo(
    offset: Long,
    timestamp: Long,
    leaderEpoch: Option[Int]
  )
  object ListOffsetsResultInfo {
    def apply(lo: JListOffsetsResultInfo): ListOffsetsResultInfo =
      ListOffsetsResultInfo(lo.offset(), lo.timestamp(), lo.leaderEpoch().toScala.map(_.toInt))
  }

  @nowarn("msg=deprecated")
  final case class ListConsumerGroupOffsetsOptions(partitions: Chunk[TopicPartition], requireStable: Boolean) {
    def asJava: JListConsumerGroupOffsetsOptions = {
      val opts = new JListConsumerGroupOffsetsOptions()
      opts.requireStable(requireStable)
      if (partitions.isEmpty) opts else opts.topicPartitions(partitions.map(_.asJava).asJava)
    }
  }

  object ListConsumerGroupOffsetsOptions {
    @deprecated("Use the listConsumerGroupOffsets overload with ListConsumerGroupOffsetsSpec", since = "2.0.5")
    def apply(partitions: Chunk[TopicPartition], requireStable: Boolean): ListConsumerGroupOffsetsOptions =
      new ListConsumerGroupOffsetsOptions(partitions, requireStable)

    @deprecated("Use the listConsumerGroupOffsets overload with ListConsumerGroupOffsetsSpec", since = "2.0.5")
    def apply(partitions: Chunk[TopicPartition]): ListConsumerGroupOffsetsOptions =
      new ListConsumerGroupOffsetsOptions(partitions, requireStable = false)

    def apply(requireStable: Boolean): ListConsumerGroupOffsetsOptions =
      new ListConsumerGroupOffsetsOptions(Chunk.empty, requireStable)
  }

  final case class ListConsumerGroupOffsetsSpec(partitions: Chunk[TopicPartition]) {
    def asJava: JListConsumerGroupOffsetsSpec = {
      val opts = new JListConsumerGroupOffsetsSpec
      opts.topicPartitions(partitions.map(_.asJava).asJava)
      opts
    }
  }

  final case class OffsetAndMetadata(
    offset: Long,
    leaderEpoch: Option[Int] = None,
    metadata: Option[String] = None
  ) {
    def asJava: JOffsetAndMetadata = new JOffsetAndMetadata(offset, leaderEpoch.map(Int.box).toJava, metadata.orNull)
  }
  object OffsetAndMetadata {
    def apply(om: JOffsetAndMetadata): OffsetAndMetadata =
      OffsetAndMetadata(
        offset = om.offset(),
        leaderEpoch = om.leaderEpoch().toScala.map(_.toInt),
        metadata = Some(om.metadata())
      )
  }

  final case class AlterConsumerGroupOffsetsOptions(timeout: Option[Duration]) {
    def asJava: JAlterConsumerGroupOffsetsOptions = {
      val options = new JAlterConsumerGroupOffsetsOptions()
      timeout.fold(options)(timeout => options.timeoutMs(timeout.toMillis.toInt))
    }
  }

  final case class ListConsumerGroupsOptions(states: Set[ConsumerGroupState]) {
    def asJava: JListConsumerGroupsOptions = new JListConsumerGroupsOptions().inStates(states.map(_.asJava).asJava)
  }

  final case class ConsumerGroupListing(groupId: String, isSimple: Boolean, state: Option[ConsumerGroupState])

  object ConsumerGroupListing {
    def apply(cg: JConsumerGroupListing): ConsumerGroupListing =
      ConsumerGroupListing(
        groupId = cg.groupId(),
        isSimple = cg.isSimpleConsumerGroup,
        state = cg.state().toScala.map(ConsumerGroupState(_))
      )
  }

  final case class KafkaConfig(entries: Map[String, ConfigEntry]) {
    def asJava: JConfig = new JConfig(entries.values.asJavaCollection)
  }
  object KafkaConfig {
    def apply(jConfig: JConfig): KafkaConfig =
      KafkaConfig(entries = jConfig.entries().asScala.map(e => e.name() -> e).toMap)
  }

  final case class LogDirDescription(error: ApiException, replicaInfos: Map[TopicPartition, ReplicaInfo])
  object LogDirDescription {
    def apply(ld: JLogDirDescription): LogDirDescription =
      LogDirDescription(
        error = ld.error(),
        replicaInfos = ld.replicaInfos().asScala.bimap(TopicPartition(_), ReplicaInfo(_)).toMap
      )
  }

  final case class ReplicaInfo(size: Long, offsetLag: Long, isFuture: Boolean)
  object ReplicaInfo {
    def apply(ri: JReplicaInfo): ReplicaInfo =
      ReplicaInfo(size = ri.size(), offsetLag = ri.offsetLag(), isFuture = ri.isFuture)
  }

  def make(settings: AdminClientSettings): ZIO[Scope, Throwable, AdminClient] =
    fromScopedJavaClient(javaClientFromSettings(settings))

  def fromJavaClient(javaClient: JAdmin): URIO[Any, AdminClient] =
    ZIO.succeed(new LiveAdminClient(javaClient))

  def fromScopedJavaClient[R, E](
    scopedJavaClient: ZIO[R & Scope, E, JAdmin]
  ): ZIO[R & Scope, E, AdminClient] =
    scopedJavaClient.flatMap { javaClient =>
      fromJavaClient(javaClient)
    }

  def javaClientFromSettings(settings: AdminClientSettings): ZIO[Scope, Throwable, JAdmin] =
    ZIO.acquireRelease {
      val endpointCheck = SslHelper
        .validateEndpoint(settings.driverSettings)

      endpointCheck *> ZIO.attempt(JAdmin.create(settings.driverSettings.asJava))
    }(client => ZIO.attempt(client.close(settings.closeTimeout)).orDie)

  implicit final class MapOps[K1, V1](private val v: Map[K1, V1]) extends AnyVal {
    def bimap[K2, V2](fk: K1 => K2, fv: V1 => V2): Map[K2, V2] = v.map { case (k, v) => fk(k) -> fv(v) }
  }

  implicit final class MutableMapOps[K1, V1](private val v: mutable.Map[K1, V1]) extends AnyVal {
    def bimap[K2, V2](fk: K1 => K2, fv: V1 => V2): mutable.Map[K2, V2] = v.map { case (k, v) => fk(k) -> fv(v) }
  }

  implicit final class OptionalOps[T](private val v: Optional[T]) extends AnyVal {
    def toScala: Option[T] = if (v.isPresent) Some(v.get()) else None
  }

  implicit final class OptionOps[T](private val v: Option[T]) extends AnyVal {
    def toJava: Optional[T] = v.fold(Optional.empty[T])(Optional.of)
  }

  implicit final class ListOps[A](private val list: List[A]) extends AnyVal {
    def forEach[B](f: A => Try[B]): Try[List[B]] = {
      @tailrec
      def loop(acc: ListBuffer[B], rest: List[A]): Try[List[B]] =
        rest match {
          case Nil => Success(acc.toList)
          case h :: t =>
            f(h) match {
              case Success(b)        => loop(acc += b, t)
              case fail @ Failure(_) => fail.asInstanceOf[Try[List[B]]]
            }
        }

      loop(ListBuffer.empty, list)
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy