Skip to content

Commit

Permalink
Merge pull request #118 from outfoxx/task/amf-update
Browse files Browse the repository at this point in the history
Use custom transformation to infer needed inheritance info for resolved document
  • Loading branch information
kdubb authored Dec 2, 2024
2 parents e6a46d8 + dc71cea commit e77665b
Show file tree
Hide file tree
Showing 9 changed files with 502 additions and 547 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:

build-test:
runs-on: macos-12
runs-on: macos-15

steps:
- uses: actions/checkout@v4
Expand All @@ -26,7 +26,7 @@ jobs:
uses: gradle/actions/setup-gradle@v4

- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_14.1.app/Contents/Developer
run: sudo xcode-select -s /Applications/Xcode_16.1.app/Contents/Developer

- name: Setup Node
uses: actions/setup-node@v4
Expand Down
6 changes: 2 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,8 @@ configure(moduleNames.map { project(it) }) {
systemProperty("junit.jupiter.execution.parallel.config.strategy", "dynamic")
systemProperty("junit.jupiter.execution.parallel.config.dynamic.factor", "3")

if (System.getenv("CI").isNullOrBlank()) {
testLogging {
events("passed", "skipped", "failed")
}
testLogging {
events("passed", "skipped", "failed")
}

reports.junitXml.required.set(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,41 @@ package io.outfoxx.sunday.generator.common
import amf.apicontract.client.platform.RAMLConfiguration
import amf.core.client.common.transform.PipelineId
import amf.core.client.common.validation.SeverityLevels
import amf.core.client.platform.AMFGraphConfiguration
import amf.core.client.platform.errorhandling.ClientErrorHandler
import amf.core.client.platform.model.document.BaseUnit
import amf.core.client.platform.model.document.Document
import amf.core.client.platform.model.domain.DomainElement
import amf.core.client.platform.model.domain.RecursiveShape
import amf.core.client.platform.model.domain.Shape
import amf.core.client.platform.transform.TransformationPipelineBuilder
import amf.core.client.platform.transform.TransformationStep
import amf.core.client.platform.validation.AMFValidationResult
import io.outfoxx.sunday.generator.utils.LocalSundayDefinitionResourceLoader
import amf.core.client.scala.errorhandling.DefaultErrorHandler
import amf.core.client.scala.model.document.FieldsFilter
import amf.core.client.scala.traversal.iterator.`DomainElementStrategy$`
import amf.core.client.scala.traversal.iterator.IdCollector
import amf.core.internal.annotations.DeclaredElement
import amf.core.internal.annotations.InheritanceProvenance
import amf.core.internal.annotations.InheritedShapes
import amf.core.internal.annotations.ResolvedLinkTargetAnnotation
import amf.core.internal.metamodel.domain.`DomainElementModel$`
import amf.core.internal.metamodel.domain.`ShapeModel$`
import amf.shapes.client.platform.model.domain.NodeShape
import io.outfoxx.sunday.generator.utils.*
import scala.Option
import scala.collection.JavaConverters.*
import scala.collection.Seq
import java.net.URI
import java.util.concurrent.ExecutionException
import kotlin.collections.set
import amf.core.client.scala.model.document.BaseUnit as InternalBaseUnit
import amf.core.client.scala.model.domain.Annotation as InternalAnnotation
import amf.core.client.scala.model.domain.DomainElement as InternalDomainElement
import amf.core.client.scala.model.domain.NamedDomainElement as InternalNamedDomainElement
import amf.core.client.scala.model.domain.extensions.PropertyShape as InternalPropertyShape
import amf.shapes.client.scala.model.domain.NodeShape as InternalNodeShape
import scala.collection.mutable.`Set$` as MutableScalaSet

open class APIProcessor {

Expand Down Expand Up @@ -69,9 +99,24 @@ open class APIProcessor {

open fun process(uri: URI): Result {

val baseConfig = RAMLConfiguration.RAML10()

val elementIds = mutableMapOf<String, InternalDomainElement>()

val pipelineId = PipelineId.Cache()

val pipeline =
TransformationPipelineBuilder
.fromPipeline(pipelineId, baseConfig)
.get()
.prepend(IndexElementsStep(elementIds))
.append(InheritanceTransformationStep(elementIds))
.build()

val ramlClient =
RAMLConfiguration.RAML10()
baseConfig
.withResourceLoader(LocalSundayDefinitionResourceLoader)
.withTransformationPipeline(pipeline)
.baseUnitClient()

val (unresolvedDocument, validationResults) =
Expand All @@ -82,12 +127,255 @@ open class APIProcessor {
throw x.cause ?: x
}

val shapeIndex = ShapeIndex.builder().index(unresolvedDocument).build()
val resolvedDocument = ramlClient.transform(unresolvedDocument, pipelineId).baseUnit() as Document

val validationReport = ramlClient.validate(unresolvedDocument).get()
val validationReport = ramlClient.validate(resolvedDocument).get()

val resolvedDocument = ramlClient.transform(unresolvedDocument, PipelineId.Cache()).baseUnit() as Document
val shapeIndex = ShapeIndex.builder().index(resolvedDocument).build()

return Result(resolvedDocument, shapeIndex, validationResults + validationReport.results())
}
}

class Inheriting(
val shapes: List<Shape>,
) : InternalAnnotation

class IndexElementsStep(
private val internalElementIds: MutableMap<String, InternalDomainElement>,
) : TransformationStep {

override fun transform(
model: BaseUnit,
errorHandler: ClientErrorHandler,
configuration: AMFGraphConfiguration,
): BaseUnit {
model.allUnits
.flatMap { it.findByType(`DomainElementModel$`.`MODULE$`.typeIris().head()) }
.forEach { shape ->
val internal = shape._internal()
internalElementIds[internal.id()] = internal
internalElementIds[internal.sourceId()] = internal
internalElementIds[internal.uniqueId()] = internal
}
return model
}
}

class InheritanceTransformationStep(
private val originalElements: Map<String, InternalDomainElement>,
) : TransformationStep {

private val clientElements = mutableMapOf<String, DomainElement>()
private val internalElements = mutableMapOf<String, InternalDomainElement>()
private val internalDeclared = mutableMapOf<String, InternalDomainElement>()
private val clientDeclared = mutableMapOf<String, DomainElement>()
private val resolvedInheriting = mutableMapOf<String, MutableSet<String>>()

private fun index(element: DomainElement) {
val internal = element._internal()
val ids = listOf(element.id(), internal.sourceId(), internal.uniqueId())
for (id in ids) {
internalElements[id] = internal
clientElements[id] = element
if (internal.annotations().contains(DeclaredElement::class.java)) {
internalDeclared[id] = internal
clientDeclared[id] = element
}
internal.resolvedLinkTargets().forEach {
internalElements[it] = internal
clientElements[it] = element
}
}
}

private fun findOriginal(element: InternalDomainElement): InternalDomainElement {
return originalElements[element.id()]
?: originalElements[element.sourceId()]
?: originalElements[element.uniqueId()]
?: error("Original element not found: ${element.id()}")
}

override fun transform(
model: BaseUnit,
errorHandler: ClientErrorHandler,
configuration: AMFGraphConfiguration,
): BaseUnit {
model.allUnits
.flatMap { it.findByType(`DomainElementModel$`.`MODULE$`.typeIris().asList.first()) }
.forEach { element ->
if (element !is RecursiveShape) {
index(element)
if (element is NodeShape) {
element.properties().forEach { index(it.range()) }
}
}
}

model._internal().transform(
{ it is InternalNodeShape },
{ shape, _ ->
var updated = shape as InternalNodeShape
updated = fixInheritance(updated)
for (property in updated.properties().asList) {
val rangeNode = property.range() as? InternalNodeShape ?: continue
fixInheritance(rangeNode)
}
Option.apply(updated)
},
DefaultErrorHandler(),
)

model._internal().transform(
{ it is InternalNodeShape },
{ shape, _ ->
var updated = shape as InternalNodeShape
updated = fixInheriting(updated)
Option.apply(updated)
},
DefaultErrorHandler(),
)
return model
}

private fun fixInheritance(node: InternalNodeShape): InternalNodeShape {
val inheritsViaAnnotation =
node.annotations().find(InheritedShapes::class.java).value?.uris()?.asList
?.mapNotNull { internalElements[it] as? InternalNodeShape }
?: listOf()
node.withInherits(inheritsViaAnnotation.asScalaSeq)

val inheritsViaProperties = fixPropertyInheritance(node)

val finalInheriting =
(inheritsViaProperties + inheritsViaAnnotation)
.distinctBy { it.sourceId() }
.filter { it.sourceId() != node.sourceId() }
for (inherited in finalInheriting) {
resolvedInheriting.getOrPut(inherited.sourceId()) { mutableSetOf() } += node.sourceId()
}

return node
}

private fun fixPropertyInheritance(node: InternalNodeShape): List<InternalNodeShape> {
val properties = node.properties().asList
val originalPropertyNames = resolveOriginalPropertyNames(node)

val inherits = mutableListOf<InternalNodeShape>()
val orderedProperties = mutableListOf<InternalPropertyShape>()
// Add inherited properties first
for (property in properties.filter { it.name().value() !in originalPropertyNames }) {
orderedProperties.add(property)
property.annotations().inheritanceProvenance().value
?.let { provenance ->
val inherited =
internalElements[provenance] as? InternalNodeShape
?: error("Provenance element not found: $provenance")
inherits.add(inherited)
if (inherited.uniqueId() != provenance) {
property.annotations()
.reject { it is InheritanceProvenance }
.`$plus$eq`(InheritanceProvenance(node.uniqueId()))
}
}
?: error("Inherited property missing provenance: ${property.id()}")
}
for (propertyName in originalPropertyNames) {
val found = properties.first { it.name().value() == propertyName }
found.annotations()
.reject { it is InheritanceProvenance }
.`$plus$eq`(InheritanceProvenance(node.uniqueId()))
orderedProperties.add(found)
}
node.withProperties(orderedProperties.asScalaSeq)
return inherits.toList()
}

private fun resolveOriginalPropertyNames(node: amf.shapes.client.scala.model.domain.NodeShape): List<String> {
val originalElement = findOriginal(node)
val originalShape =
originalElement as? InternalNodeShape
?: error("Original element not a NodeShape: ${originalElement.id()}")
val originalPropertyNames =
originalShape.properties().asList
.map { it.name().value() }
return originalPropertyNames
}

private fun fixInheriting(shape: InternalNodeShape): InternalNodeShape {
val resolved = resolvedInheriting[shape.sourceId()] ?: return shape
if (resolved.isNotEmpty()) {
val inheriting =
resolved
.mapNotNull { inherit ->
val node =
when (val found = clientDeclared[inherit]) {
null -> null
is NodeShape -> found
is RecursiveShape -> clientElements[found.fixpoint().value] as NodeShape?
else -> error("Inheriting shape not a NodeShape: $inherit")
}
if (node != null && node.inherits().any { it.name == shape.name().value() })
node
else
null
}
shape.annotations().`$plus$eq`(Inheriting(inheriting))
}
return shape
}
}

inline fun <reified T> InternalBaseUnit.filterIsInstance(): Sequence<T> {
return asJavaIterator(
this.iterator(
`DomainElementStrategy$`.`MODULE$`,
FieldsFilter.`All$`.`MODULE$`,
IdCollector(MutableScalaSet.`MODULE$`.empty()),
),
)
.asSequence()
.filterIsInstance<T>()
}

private val <T> Option<T>.value: T?
get() = if (isDefined) get() else null

private val <T> Seq<T>.asList: List<T>
get() = seqAsJavaList(this)

private val <T> Seq<T>.asSequence: Sequence<T>
get() = asJavaIterable(this).asSequence()

private val <T> Iterable<T>.asScalaSeq: Seq<T>
get() = asScalaIterator(iterator()).toSeq()

private fun InternalDomainElement.resolvedLinkTargets(): List<String> =
annotations().serializables().asList.filterIsInstance<ResolvedLinkTargetAnnotation>()
.map { it.linkTargetId() }

private fun InternalDomainElement.sourceId(): String =
annotations().sourceLocation().toString()

private fun InternalDomainElement.uniqueIdLocation(): String? =
fields()[`ShapeModel$`.`MODULE$`.Name()]
?.let { name ->
name.location().value?.ifBlank { null }
}
?: annotations().location().value

private fun InternalDomainElement.uniqueId(): String =
if (this is InternalNamedDomainElement) {
uniqueIdLocation()
?.let { location ->
annotations().fragmentName().value
?.let { fragmentName ->
"$location#$fragmentName/${name().value()}"
}
?: "$location/${name().value()}"
}
?: id()
} else {
id()
}
Loading

0 comments on commit e77665b

Please sign in to comment.