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

tscfg.Struct.scala Maven / Gradle / Ivy

package tscfg

import com.typesafe.config.{Config, ConfigValueType}
import tscfg.DefineCase._
import tscfg.exceptions.ObjectDefinitionException
import tscfg.ns.Namespace

import scala.annotation.tailrec
import scala.collection.{Map, mutable}

/** Supports a convenient next representation based on given TS Config object.
  * It supports nested member definitions utilizing the 'members' field
  *
  * @param name
  *   Name of the config member
  * @param members
  *   Nested config definitions
  * @param tsStringValue
  *   Captures string value to support determining dependencies in terms of RHS
  *   names (that is, when such a string may be referring to a @define)
  */
case class Struct(
    name: String,
    members: mutable.HashMap[String, Struct] = mutable.HashMap.empty,
    tsStringValue: Option[String] = None
) {

  // Non-None when this is a `@define`
  var defineCaseOpt: Option[DefineCase] = None

  def isDefine: Boolean = defineCaseOpt.isDefined

  def isExtends: Boolean = defineCaseOpt match {
    case Some(_: ExtendsDefineCase)    => true
    case Some(_: ImplementsDefineCase) => true
    case _                             => false
  }

  def isEnum: Boolean = defineCaseOpt.exists(_.isEnum)

  def isLeaf: Boolean = members.isEmpty

  def dependencies: Set[String] = {
    tsStringValue.toSet ++ members.values.flatMap(_.dependencies)
  }

  // $COVERAGE-OFF$
  def format(indent: String = ""): String = {
    val defineStr = defineCaseOpt.map(dc => s" $dc").getOrElse("")
    val nameStr   = s"${if (name.isEmpty) "(root)" else name}$defineStr"

    val dependenciesStr = dependencies.toList match {
      case Nil => ""
      case l   => s" [dependencies=${l.mkString(", ")}]"
    }

    val nameHeading = nameStr + dependenciesStr

    if (members.isEmpty) {
      nameHeading
    }
    else {
      val indent2 = indent + "    "
      s"$nameHeading ->\n" + indent2 + {
        members
          .map(e => s"${e._1}: " + e._2.format(indent2))
          .mkString("\n" + indent2)
      }
    }
  }
  // $COVERAGE-ON$
}

object Struct {
  import scala.jdk.CollectionConverters._

  /** Gets all structs from the given TS Config object, sorted appropriately for
    * subsequent processing in ModelBuilder. Any circular reference will throw a
    * [[ObjectDefinitionException]].
    */
  def getMemberStructs(namespace: Namespace, conf: Config): List[Struct] = {
    val struct: Struct              = getStruct(conf)
    val memberStructs: List[Struct] = struct.members.values.toList

    // set any define to each struct:
    memberStructs.flatMap { setDefineCase(conf, _) }

    val (defineStructs, nonDefineStructs) = memberStructs.partition(_.isDefine)
    val sortedDefineStructs               = sortDefineStructs(defineStructs)

    val sortedStructs = {
      // but also sort the "defines" by any name (member type) dependencies:
      val definesSortedByNameDependencies = sortByNameDependencies(
        sortedDefineStructs
      )
      definesSortedByNameDependencies ++ nonDefineStructs
    }

    if (namespace.isRoot) {
      scribe.debug(
        s"root\n" +
          s"struct=${struct.format()}\n" +
          s"sortedStructs=\n  ${sortedStructs.map(_.format()).mkString("\n  ")}"
      )
    }

    sortedStructs
  }

  private def sortDefineStructs(defineStructs: List[Struct]): List[Struct] = {
    val sorted = mutable.LinkedHashMap.empty[String, Struct]

    // / `othersBeingAdded` allows to check for circularity
    def addExtendStruct(
        s: Struct,
        othersBeingAdded: List[Struct] = List.empty
    ): Unit = {
      def addExtendsOrImplements(name: String, isExternal: Boolean): Unit = {
        sorted.get(name) match {
          case Some(_) =>
            // ancestor already added. Add this descendent struct:
            sorted.put(s.name, s)

          case None =>
            // ancestor not added yet. Find it in base list:
            defineStructs.find(_.name == name) match {
              case Some(ancestor) =>
                // check for any circularity:
                if (othersBeingAdded.exists(_.name == ancestor.name)) {
                  val path = s :: othersBeingAdded
                  val via =
                    path.reverseIterator
                      .map("'" + _.name + "'")
                      .mkString(" -> ")
                  throw ObjectDefinitionException(
                    s"extension of struct '${s.name}' involves circular reference via $via"
                  )
                }

                // ok, add ancestor:
                addExtendStruct(ancestor, s :: othersBeingAdded)
                // and then add this struct:
                sorted.put(s.name, s)

              case None if isExternal =>
                sorted.put(s.name, s)

              case None =>
                throw ObjectDefinitionException(
                  s"struct '${s.name}' with undefined extend '$name'"
                )
            }
        }
      }

      s.defineCaseOpt.get match {
        case SimpleDefineCase | AbstractDefineCase | EnumDefineCase =>
          sorted.put(s.name, s)

        case c: ExtendsDefineCase =>
          addExtendsOrImplements(c.name, c.isExternal)

        case c: ImplementsDefineCase =>
          addExtendsOrImplements(c.name, c.isExternal)
      }
    }

    defineStructs.foreach(addExtendStruct(_))

    assert(
      defineStructs.size == sorted.size,
      s"defineStructs.size=${defineStructs.size} != sorted.size=${sorted.size}"
    )

    sorted.toList.map(_._2)
  }

  private def sortByNameDependencies(structs: List[Struct]): List[Struct] = {
    structs.sortWith((a, b) => {
      val aDeps = a.dependencies
      val bDeps = b.dependencies

      if (aDeps.contains(b.name)) {
        // a depends on b, so b should come first:
        false
      }
      else if (bDeps.contains(a.name)) {
        // b depends on a, so a should come first:
        true
      }
      else {
        // no dependency, so sort by name:
        a.name < b.name
      }
    })
  }

  /** Determines the joint set of all ancestor's members to allow proper
    * overriding in child structs.
    *
    * Note that not-circularity is verified prior to calling this function.
    *
    * @param struct
    *   Current (child) struct to consider
    * @param memberStructs
    *   List to find referenced structs
    * @param namespace
    *   Current known name space
    * @return
    *   Mapping from symbol to type definition if struct is an ExtendsDefineCase
    */
  def ancestorClassMembers(
      struct: Struct,
      memberStructs: List[Struct],
      namespace: Namespace
  ): Option[Map[String, model.AnnType]] = {

    def handleExtends(
        parentName: String,
        isExternal: Boolean
    ): Option[Map[String, model.AnnType]] = {
      val defineStructs = memberStructs.filter(_.isDefine)

      val greatAncestorMembers =
        defineStructs.find(_.name == parentName) match {
          case Some(parentStruct) if parentStruct.isExtends =>
            ancestorClassMembers(parentStruct, memberStructs, namespace)

          case Some(_) => None

          case None if isExternal => None

          case None =>
            throw new RuntimeException(
              s"struct '${struct.name}' with undefined extend '$parentName'"
            )
        }

      val parentMembers =
        namespace.getRealDefine(parentName).map(_.members) match {
          case Some(parentMembers) => parentMembers

          case None if isExternal => None

          case None =>
            throw new IllegalArgumentException(
              s"@define '${struct.name}' is invalid because '$parentName' is not @defined"
            )
        }

      // join both member maps
      Some(greatAncestorMembers.getOrElse(Map.empty) ++ parentMembers)
    }

    struct.defineCaseOpt.flatMap {
      case c: ExtendsDefineCase =>
        handleExtends(c.name, c.isExternal)

      case c: ImplementsDefineCase =>
        // note: handling it as an extends (todo: revisit this at some point)
        handleExtends(c.name, c.isExternal)

      case _ => None
    }
  }

  private def getStruct(conf: Config): Struct = {
    val structs = mutable.HashMap[String, Struct]("" -> Struct(""))

    def resolve(key: String): Struct = {
      if (!structs.contains(key)) structs.put(key, Struct(getSimple(key)))
      structs(key)
    }

    // Due to TS Config API, we traverse from the leaves to the ancestors:
    conf.entrySet().asScala foreach { e =>
      val path        = e.getKey
      val configValue = e.getValue

      // capture string value to determine possible "define" dependency
      val tsStringValue: Option[String] = e.getValue.valueType() match {
        case ConfigValueType.STRING => Some(configValue.unwrapped().toString)
        case _                      => None
      }
      scribe.debug(s"getStruct: path=$path, tsStringValue=$tsStringValue")
      val leaf = Struct(path, tsStringValue = tsStringValue)

      doAncestorsOf(path, leaf)

      def doAncestorsOf(childKey: String, childStruct: Struct): Unit = {
        val (parent, simple) = (getParent(childKey), getSimple(childKey))
        createParent(parent, simple, childStruct)

        @tailrec
        def createParent(
            parentKey: String,
            simple: String,
            child: Struct
        ): Unit = {
          val parentGroup = resolve(parentKey)
          parentGroup.members.put(simple, child)
          if (parentKey != "") {
            createParent(
              getParent(parentKey),
              getSimple(parentKey),
              parentGroup
            )
          }
        }
      }
    }

    def getParent(path: String): String = {
      val idx = path.lastIndexOf('.')
      if (idx >= 0) path.substring(0, idx) else ""
    }

    def getSimple(path: String): String = {
      val idx = path.lastIndexOf('.')
      if (idx >= 0) path.substring(idx + 1) else path
    }

    structs("")
  }

  private def setDefineCase(conf: Config, s: Struct): Option[DefineCase] = {
    val cv          = conf.getValue(s.name)
    val comments    = cv.origin().comments().asScala.toList
    val defineLines = comments.map(_.trim).filter(_.startsWith("@define"))
    s.defineCaseOpt = defineLines.length match {
      case 0 => None
      case 1 => DefineCase.getDefineCase(defineLines.head)
      case _ =>
        throw new IllegalArgumentException(
          s"multiple @define lines for ${s.name}."
        )
    }
    s.defineCaseOpt
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy