Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

publish amf-core 5.5.0 #676

Merged
merged 15 commits into from
Apr 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#GUSINFO:MS AMF,MS AMF
#GUSINFO:MS Calypso,MS Calypso
* @aml-org/team-amf
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v16.17.1
5 changes: 5 additions & 0 deletions .sdkmanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17.0.10-tem
scala=2.12.15
sbt=1.7.3
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ val ivyLocal = Resolver.file("ivy", file(Path.userHome.absolutePath + "/.ivy2/lo

name := "amf-core"
ThisBuild / scalaVersion := "2.12.15"
ThisBuild / version := "5.4.9"
ThisBuild / version := "5.5.0"

publish := {}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package amf.core.internal.annotations

import amf.core.client.scala.model.domain._

/*
This annotation is used in Async 2.4 to differentiate parameter declarations from Server Variable declarations
*/
case class DeclaredServerVariable() extends SerializableAnnotation with PerpetualAnnotation {
override val name: String = "declared-server-variable"
override val value: String = ""
}

object DeclaredServerVariable extends AnnotationGraphLoader {
override def unparse(value: String, objects: Map[String, AmfElement]): Option[Annotation] =
Some(DeclaredServerVariable())
}
14 changes: 2 additions & 12 deletions shared/src/main/scala/amf/core/internal/remote/Cache.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package amf.core.internal.remote

import amf.core.client.scala.model.document.BaseUnit
import amf.core.internal.utils.GraphCycleDetector

import scala.collection.mutable
import scala.concurrent.{ExecutionContext, Future}
Expand All @@ -19,17 +20,6 @@ class Cache {
dependencyGraph.update(to, fromNodes.+(from))
}

protected def findCycles(node: String, acc: Set[String] = Set()): Boolean = {
if (acc.contains(node)) {
true
} else {
val sources = dependencyGraph.getOrElse(node, Set())
sources.exists { source =>
findCycles(source, acc + node)
}
}
}

protected def beforeLast(elms: List[String]): Option[String] = {
val lastTwo = elms.takeRight(2)
if (lastTwo.size == 2) {
Expand All @@ -45,7 +35,7 @@ class Cache {
beforeLast(context.history) foreach { from =>
addFromToEdge(from, url)
}
if (findCycles(url)) {
if (GraphCycleDetector.hasCycles(dependencyGraph, url)) {
if (cache(url).isCompleted) {
cache(url)
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package amf.core.internal.transform.stages
import amf.core.client.scala.AMFGraphConfiguration
import amf.core.client.scala.errorhandling.AMFErrorHandler
import amf.core.internal.metamodel.Type.{ArrayLike, Iri}
import amf.core.internal.metamodel.domain.LinkableElementModel
import amf.core.client.scala.model.document.BaseUnit
import amf.core.client.scala.model.domain._
import amf.core.client.scala.transform.TransformationStep
import amf.core.internal.parser.domain.FieldEntry
import amf.core.client.scala.vocabulary.Namespace
import amf.core.internal.metamodel.Type.{ArrayLike, Iri}
import amf.core.internal.metamodel.domain.LinkableElementModel
import amf.core.internal.parser.domain.{Annotations, FieldEntry, Value}

import scala.collection.mutable
Expand All @@ -19,17 +18,23 @@ class UrlShortenerStage() extends TransformationStep {
errorHandler: AMFErrorHandler,
configuration: AMFGraphConfiguration
): BaseUnit = {
val ids: Set[String] = Set(model.id) ++ obtainNestedReferenceIds(model)
val ids: mutable.Set[String] = mutable.Set()
collectNestedReferenceIdsInSet(model, ids)
ids.add(model.id)

shorten(model, ids)
model.withId(base)
}

private def obtainNestedReferenceIds[T <: BaseUnit](model: T): Seq[String] = {
val ids = model.references.map(_.id)
ids ++ model.references.flatMap(obtainNestedReferenceIds)
private def collectNestedReferenceIdsInSet[T <: BaseUnit](model: T, set: mutable.Set[String]): Unit = {
if (!set.contains(model.id)) {
set += model.id

model.references.foreach(collectNestedReferenceIdsInSet(_, set))
}
}

def shorten(element: AmfElement, ids: Set[String]): Unit = {
def shorten(element: AmfElement, ids: Traversable[String]): Unit = {
element match {
case o: AmfObject =>
val shorthenId = shortener.shorten(o.id)
Expand Down Expand Up @@ -64,7 +69,7 @@ class UrlShortenerStage() extends TransformationStep {
shorten(element.annotations)
}

private def shortenIriValue(element: AmfElement, ids: Set[String]): AmfElement = {
private def shortenIriValue(element: AmfElement, ids: Traversable[String]): AmfElement = {
val stringValue = element.toString
if (ids.exists(i => stringValue.startsWith(i)))
AmfScalar(shortener.shorten(stringValue), element.annotations)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package amf.core.internal.utils

import scala.annotation.tailrec
import scala.collection.mutable

object GraphCycleDetector {
type Graph[N] = mutable.Map[N, Set[N]]

private case class NodeWithBranch[N](node: N, branch: List[N])

def hasCycles[N](graph: Graph[N], initialNode: N): Boolean = {
// Mutable collections are used to improve performance
val allVisited = mutable.Set[N]()

@tailrec
def innerHasCycles(pendingNodes: List[NodeWithBranch[N]]): Boolean = pendingNodes match {
case Nil => false
case ::(NodeWithBranch(currentNode, currentBranch), remainingNodes) =>
if (currentBranch.contains(currentNode)) {
// There is a cycle
return true
}

if (allVisited.contains(currentNode)) {
// This node has already been visited
innerHasCycles(remainingNodes)
} else {
allVisited.add(currentNode)
val nextBranch: List[N] = currentNode +: currentBranch

val newNodes = graph.getOrElse(currentNode, Set()).toList
val newNodesWithBranches = newNodes.map { NodeWithBranch(_, nextBranch) }

val newPendingNodes: List[NodeWithBranch[N]] = newNodesWithBranches ::: pendingNodes
innerHasCycles(newPendingNodes)
}
}

innerHasCycles(List(NodeWithBranch(initialNode, List())))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package amf.core.internal.utils

import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

import scala.collection.mutable

class GraphCycleDetectorTest extends AnyFunSuite with Matchers {

test("Simple linear graph should not have cycles") {
val graph = mutable.Map(
"A" -> Set("B"),
"B" -> Set("C"),
"C" -> Set("D")
)

GraphCycleDetector.hasCycles(graph, "A") shouldBe false
}

test("Simple linear graph should not have cycles starting in other node") {
val graph = mutable.Map(
"A" -> Set("B"),
"B" -> Set("C"),
"C" -> Set("D")
)

GraphCycleDetector.hasCycles(graph, "B") shouldBe false
}

test("Graph with simple cycle should be detected from A") {
val graph = mutable.Map(
"A" -> Set("B"),
"B" -> Set("C"),
"C" -> Set("A")
)

GraphCycleDetector.hasCycles(graph, "A") shouldBe true
}

test("Graph with simple cycle should be detected from B") {
val graph = mutable.Map(
"A" -> Set("B"),
"B" -> Set("C"),
"C" -> Set("A")
)

GraphCycleDetector.hasCycles(graph, "B") shouldBe true
}

test("Graph with branches should not have cycles") {
val graph = mutable.Map(
"A" -> Set("B"),
"A" -> Set("C"),
"B" -> Set("D"),
"B" -> Set("E"),
"C" -> Set("F"),
"C" -> Set("G")
)

GraphCycleDetector.hasCycles(graph, "A") shouldBe false
}

test("Graph with shared parents should not have cycles") {
val graph = mutable.Map(
"A" -> Set("B"),
"A" -> Set("C"),
"B" -> Set("D"),
"B" -> Set("E"),
"C" -> Set("D"),
"C" -> Set("E"),
"D" -> Set("F"),
"E" -> Set("F"),
"F" -> Set("G")
)

GraphCycleDetector.hasCycles(graph, "A") shouldBe false
}

test("Graph with shared parents and cycles should be detected") {
val graph = mutable.Map(
"A" -> Set("B"),
"A" -> Set("C"),
"B" -> Set("D"),
"B" -> Set("E"),
"C" -> Set("D"),
"C" -> Set("E"),
"D" -> Set("F"),
"E" -> Set("F"),
"F" -> Set("A")
)

GraphCycleDetector.hasCycles(graph, "A") shouldBe true
}

test("Graph with shared parents and cycles should be detected starting from other node") {
val graph = mutable.Map(
"A" -> Set("B"),
"A" -> Set("C"),
"B" -> Set("D"),
"B" -> Set("E"),
"C" -> Set("D"),
"C" -> Set("E"),
"D" -> Set("F"),
"E" -> Set("F"),
"F" -> Set("A")
)

GraphCycleDetector.hasCycles(graph, "B") shouldBe true
}

test("Graph with shared parents and cycles should not be detected is starting from node without cycles") {
val graph = mutable.Map(
"A" -> Set("B"),
"A" -> Set("C"),
"B" -> Set("D"),
"B" -> Set("E"),
"B" -> Set("G"),
"G" -> Set("H"),
"G" -> Set("I"),
"C" -> Set("D"),
"C" -> Set("E"),
"D" -> Set("F"),
"E" -> Set("F"),
"F" -> Set("A")
)

GraphCycleDetector.hasCycles(graph, "G") shouldBe false
}

}