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

scalapb.compiler.FileOptionsCache.scala Maven / Gradle / Ivy

The newest version!
package scalapb.compiler
import com.google.protobuf.Descriptors.FileDescriptor
import scalapb.options.Scalapb
import scalapb.options.Scalapb.ScalaPbOptions
import scalapb.options.Scalapb.ScalaPbOptions.OptionsScope

import scala.collection.mutable
import scala.jdk.CollectionConverters._
import scala.util.Success
import scala.util.Failure
import scalapb.options.Scalapb.PreprocessorOutput

object FileOptionsCache {
  def parentPackages(packageName: String): List[String] = {
    packageName
      .split('.')
      .scanLeft(Seq[String]())(_ :+ _)
      .drop(1)
      .dropRight(1)
      .map(_.mkString("."))
      .reverse
      .toList
  }

  def mergeOptions(parent: ScalaPbOptions, child: ScalaPbOptions) = {
    val r = ScalaPbOptions
      .newBuilder(parent)
      .mergeFrom(child)
      .setScope(child.getScope) // retain child's scope
    r.build()
  }

  def reducePackageOptions[T](
      files: Seq[(FileDescriptor, ScalaPbOptions)],
      data: (FileDescriptor, ScalaPbOptions) => T
  )(op: (T, T) => T): Map[FileDescriptor, T] = {
    val byPackage = new mutable.HashMap[String, T]
    val output    = new mutable.HashMap[FileDescriptor, T]

    // Process files by package name, and process package scoped options for each package first.
    files
      .sortBy(f => (f._1.getPackage(), f._2.getScope != OptionsScope.PACKAGE))
      .foreach { case (f, opts) =>
        val isPackageScoped = (opts.getScope == OptionsScope.PACKAGE)
        if (isPackageScoped && f.getPackage().isEmpty())
          throw new GeneratorException(
            s"${f.getFullName()}: a package statement is required when package-scoped options are used"
          )
        if (isPackageScoped && byPackage.contains(f.getPackage())) {
          val dups = files
            .filter(other =>
              other._1.getPackage() == f.getPackage() && other._2
                .getScope() == OptionsScope.PACKAGE
            )
            .map(_._1.getFullName())
            .mkString(", ")
          throw new GeneratorException(
            s"Multiple files contain package-scoped options for package '${f.getPackage}': ${dups}"
          )
        }

        if (isPackageScoped && opts.hasObjectName())
          throw new GeneratorException(
            s"${f.getFullName()}: object_name is not allowed in package-scoped options."
          )

        val packagesToInheritFrom =
          if (isPackageScoped) parentPackages(f.getPackage())
          else f.getPackage() :: parentPackages(f.getPackage())

        val inherited = packagesToInheritFrom.find(byPackage.contains(_)).map(byPackage(_))

        val res = inherited match {
          case Some(base) => op(base, data(f, opts))
          case None       => data(f, opts)
        }
        output += f -> res
        if (isPackageScoped) {
          byPackage += f.getPackage -> res
        }
      }
    output.toMap
  }

  // For each file, which preprocessors are enabled
  def preprocessorsForFile(files: Seq[FileDescriptor]): Map[FileDescriptor, Seq[String]] =
    reducePackageOptions[Seq[String]](
      files.map(f => (f, f.getOptions.getExtension(Scalapb.options))),
      (_, opts) => opts.getPreprocessorsList.asScala.toSeq
    )((parent, child) => clearNegatedPreprocessors(parent ++ child))

  @deprecated(
    "Use buildCache that takes SecondaryOutputProvider. Preprocessors will not work",
    "0.10.10"
  )
  def buildCache(
      files: Seq[FileDescriptor]
  ): Map[FileDescriptor, ScalaPbOptions] = buildCache(files, SecondaryOutputProvider.empty)

  // Given a list of preprocessors, if it contains an opted-out preprocessor (in the form of -$name),
  // then it removes it from the list.
  private def clearNegatedPreprocessors(input: Seq[String]): Seq[String] = {
    val excludedPreprocessors = input.filter(_.startsWith("-")).map(_.tail)
    input.filter(p => !excludedPreprocessors.contains(p) && !p.startsWith("-"))
  }

  def buildCache(
      filesIn: Seq[FileDescriptor],
      secondaryOutputProvider: SecondaryOutputProvider
  ): Map[FileDescriptor, ScalaPbOptions] = {
    val preprocessorsByFile: Map[FileDescriptor, Seq[String]] = preprocessorsForFile(filesIn)

    for {
      (file, names) <- preprocessorsByFile
      name          <- names
    } {
      if (!SecondaryOutputProvider.isNameValid(name))
        throw new GeneratorException(
          s"${file.getFullName()}: Invalid preprocessor name: '$name'"
        )
    }

    val allPreprocessorNames: Set[String] = preprocessorsByFile.values.flatten.toSet
    val preprocessorValues: Map[String, PreprocessorOutput] = allPreprocessorNames.map { name =>
      secondaryOutputProvider.get(name) match {
        case Success(output) =>
          FileOptionsCache.validatePreprocessorOutput(name, output)
          name -> output
        case Failure(exception) =>
          val files = preprocessorsByFile
            .collect {
              case (fd, preprocessors) if preprocessors.contains(name) => fd.getFullName()
            }
            .mkString(", ")
          throw GeneratorException(s"$files: ${exception.getMessage()}")
      }
    }.toMap

    val processedOptions: Map[FileDescriptor, ScalaPbOptions] = preprocessorsByFile.map {
      case (file, preprocessorsForFile) =>
        file -> preprocessorsForFile
          .flatMap(name =>
            Option(preprocessorValues(name).getOptionsByFileMap.get(file.getFullName()))
          )
          .foldRight(file.getOptions().getExtension(Scalapb.options))(mergeOptions(_, _))
    }.toMap

    val fileOptions = reducePackageOptions[ScalaPbOptions](
      filesIn.map(f => (f, processedOptions(f))),
      (_, opts) => opts
    )(
      mergeOptions(_, _)
    )

    val fieldTransformations = reducePackageOptions[Seq[ResolvedFieldTransformation]](
      filesIn.map(f => (f, processedOptions(f))),
      (file, opts) =>
        opts
          .getFieldTransformationsList()
          .asScala
          .map(t =>
            ResolvedFieldTransformation(
              file,
              t
            )
          )
          .toSeq
    )(_ ++ _)

    fileOptions.map { case (f, opts) =>
      f ->
        (if (opts.getIgnoreAllTransformations) opts
         else
           opts.toBuilder
             .addAllAuxFieldOptions(
               FieldTransformations
                 .processFieldTransformations(f, fieldTransformations(f))
                 .asJava
             )
             .build())
    }.toMap
  }

  private[scalapb] def validatePreprocessorOutput(
      name: String,
      output: PreprocessorOutput
  ): PreprocessorOutput = {
    output.getOptionsByFileMap().asScala.find(_._2.getScope() != OptionsScope.FILE).foreach { ev =>
      throw new GeneratorException(
        s"Preprocessor options must be file-scoped. Preprocessor '${name}' provided scope '${ev._2
            .getScope()}' for file ${ev._1}."
      )
    }
    output
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy