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

W-10881317/computed-instance-parsing: implemented instance parsing for allOf, oneOf and if-then-else #499

Merged
merged 2 commits into from
Apr 1, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import amf.aml.internal.metamodel.document.DialectInstanceModel
import amf.aml.client.scala.model.document.{DialectInstanceFragment, DialectInstanceProcessingData}
import amf.aml.client.scala.model.domain.{DialectDomainElement, DocumentsModel}
import amf.aml.internal.parse.instances.DialectInstanceParser.encodedElementDefaultId
import amf.aml.internal.parse.instances.parser.InstanceNodeParser
import amf.aml.internal.parse.instances.parser.InstanceElementParser
import amf.aml.internal.validate.DialectValidations.DialectError
import com.github.ghik.silencer.silent

Expand Down Expand Up @@ -47,7 +47,7 @@ class DialectInstanceFragmentParser(root: Root)(implicit override val ctx: Diale
ctx.findNodeMapping(documentMapping.encoded().value()) match {
case Some(nodeMapping) =>
val path = dialectInstanceFragment.id + "#"
InstanceNodeParser(root).parse(path, path + "/", map, nodeMapping, Map(), givenAnnotations = None)
InstanceElementParser(root).parse(path, path + "/", map, nodeMapping, Map(), givenAnnotations = None)
case _ =>
emptyElementWithViolation(s"Could not find node mapping for: ${documentMapping.encoded().value()}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,11 @@ class DialectInstanceParser(val root: Root)(implicit val ctx: DialectInstanceCon
entry.value.as[YMap].entries.foreach { declarationEntry =>
val declarationName = declarationEntry.key.as[YScalar].text
val id = pathSegment(declarationsId, List(declarationName))
val node = InstanceNodeParser(root).parse(declarationsId,
id,
declarationEntry.value,
nodeMapping,
givenAnnotations = Some(Annotations(declarationEntry)))
val node = InstanceElementParser(root).parse(declarationsId,
id,
declarationEntry.value,
nodeMapping,
givenAnnotations = Some(Annotations(declarationEntry)))

// lookup by ref name
node.set(DialectDomainElementModel.DeclarationName,
Expand All @@ -187,13 +187,13 @@ class DialectInstanceParser(val root: Root)(implicit val ctx: DialectInstanceCon
val additionalKey =
if (documents.keyProperty().value()) Some(ctx.dialect.name().value())
else None
InstanceNodeParser(root).parse(path,
encodedElementDefaultId(dialectInstance),
map,
nodeMapping,
rootNode = true,
givenAnnotations = None,
additionalKey = additionalKey)
InstanceElementParser(root).parse(path,
encodedElementDefaultId(dialectInstance),
map,
nodeMapping,
rootNode = true,
givenAnnotations = None,
additionalKey = additionalKey)
case _ =>
emptyElementWithViolation(s"Could not find node mapping for: ${mapping.encoded().value()}")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,23 +145,6 @@ object InstanceNodeIdHandling {

trait InstanceNodeIdHandling extends BaseDirectiveOverride { this: DialectInstanceParser =>

protected def generateNodeId(node: DialectDomainElement,
nodeMap: YMap,
path: Seq[String],
defaultId: String,
mapping: NodeMapping,
additionalProperties: Map[String, Any] = Map(),
rootNode: Boolean): String = {
InstanceNodeIdHandling.generateNodeId(node,
nodeMap,
path,
defaultId,
mapping,
additionalProperties,
rootNode,
root)
}

protected def idTemplate(node: DialectDomainElement,
nodeMap: YMap,
path: Seq[String],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ object DialectExtensionParser {
ctx.nestedDialects ++= Seq(dialect)
ctx.withCurrentDialect(dialect) {
val dialectDomainElement =
nodeParser(id, nestedObjectId, propertyEntry.value, nodeMapping, Map.empty)
nodeParser(id, nestedObjectId, propertyEntry.value, nodeMapping, Map.empty, false)
node.withObjectField(property, dialectDomainElement, Right(propertyEntry))
}
case None =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,29 @@ import amf.aml.internal.parse.instances.ClosedInstanceNode.checkNode
import amf.aml.internal.parse.instances.DialectInstanceParser.{computeParsingScheme, emptyElement, typesFrom}
import amf.aml.internal.parse.instances.InstanceNodeIdHandling.generateNodeId
import amf.aml.internal.parse.instances.parser.IncludeNodeParser.resolveLink
import amf.aml.internal.parse.instances.parser.applicable.ApplicableMappingFinder
import amf.aml.internal.parse.instances.{DialectInstanceContext, NodeMappableHelper}
import amf.aml.internal.validate.DialectValidations.DialectError
import amf.core.client.common.validation.AMFStyle
import amf.core.client.scala.errorhandling.DefaultErrorHandler
import amf.core.client.scala.model.domain.DomainElement
import amf.core.client.scala.parse.document.SyamlParsedDocument
import amf.core.client.scala.validation.AMFValidationResult
import amf.core.internal.parser.domain.Annotations
import amf.core.internal.parser.{Root, YMapOps}
import amf.core.internal.utils.AmfStrings
import amf.validation.internal.shacl.custom.CustomShaclValidator
import org.yaml.model._

case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext) extends NodeMappableHelper {
import scala.language.{higherKinds, implicitConversions}

case class InstanceElementParser(root: Root)(implicit ctx: DialectInstanceContext) extends NodeMappableHelper {

private val map: YMap = root.parsed.asInstanceOf[SyamlParsedDocument].document.as[YMap]

def parse(path: String, id: String, entry: YNode, mapping: NodeMappable, additionalProperties: Map[String, Any])(
implicit ctx: DialectInstanceContext): DialectDomainElement =
parse(path, id, entry, mapping, additionalProperties, givenAnnotations = None)
def parse(path: String,
id: String,
entry: YNode,
mapping: NodeMappable,
additionalProperties: Map[String, Any],
parseAllOf: Boolean)(implicit ctx: DialectInstanceContext): DialectDomainElement =
parse(path, id, entry, mapping, additionalProperties, givenAnnotations = None, parseAllOf = parseAllOf)

def parse(path: String,
defaultId: String,
Expand All @@ -35,7 +38,7 @@ case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext)
rootNode: Boolean = false,
givenAnnotations: Option[Annotations],
additionalKey: Option[String] = None,
)(implicit ctx: DialectInstanceContext): DialectDomainElement = {
parseAllOf: Boolean = true)(implicit ctx: DialectInstanceContext): DialectDomainElement = {
val result: DialectDomainElement = ast.tagType match {
case YType.Map =>
val astMap = ast.as[YMap]
Expand All @@ -47,7 +50,8 @@ case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext)
additionalProperties,
rootNode,
givenAnnotations,
additionalKey)
additionalKey,
parseAllOf)

case YType.Str => resolveLink(ast, mappable, defaultId, givenAnnotations)
case YType.Include => resolveLink(ast, mappable, defaultId, givenAnnotations)
Expand All @@ -59,8 +63,11 @@ case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext)
// if we are parsing a patch document we mark the node as abstract

mappable match {
case anyMappable: AnyMapping if anyMappable.and.nonEmpty || anyMappable.or.nonEmpty || anyMappable.ifMapping.option().nonEmpty => // don't do anything
case _ => assignDefinedByAndTypes(mappable, result)
case anyMappable: AnyMapping
if anyMappable.and.nonEmpty || anyMappable.or.nonEmpty || anyMappable.ifMapping
.option()
.nonEmpty => // don't do anything
case _ => assignDefinedByAndTypes(mappable, result)
}
if (ctx.isPatch) result.withAbstract(true)
result
Expand Down Expand Up @@ -94,14 +101,31 @@ case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext)
additionalProperties: Map[String, Any],
rootNode: Boolean,
givenAnnotations: Option[Annotations],
additionalKey: Option[String])(implicit ctx: DialectInstanceContext) = {
additionalKey: Option[String],
parseAllOf: Boolean = true)(implicit ctx: DialectInstanceContext) = {
computeParsingScheme(astMap) match {
case "$ref" => RefNodeParser.parse(defaultId, astMap, mappable, root)
case "$include" => IncludeNodeParser.parse(ast, mappable, defaultId, givenAnnotations)
case _ =>
mappable match {
case conditional: AnyMapping if conditional.ifMapping.nonEmpty =>
parseConditionally(path, defaultId, astMap, conditional)
case any: AnyMapping
if parseAllOf && (any.and.nonEmpty || any.or.nonEmpty || any.ifMapping.option().nonEmpty) =>
val applicableMapping = ApplicableMappingFinder(root).find(map, any)
applicableMapping
.map { foundMapping =>
parseWithNodeMapping(defaultId,
astMap,
ast,
additionalProperties,
rootNode,
givenAnnotations,
additionalKey,
foundMapping)
}
.getOrElse {
// TODO: add error
emptyElement(defaultId, astMap, mappable, givenAnnotations)
}
case mapping: NodeMapping =>
parseWithNodeMapping(defaultId,
astMap,
Expand All @@ -118,61 +142,6 @@ case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext)
}
}

object IfThenElseBranchCriteria {
def choose(map: YMap, mapping: AnyMapping)(implicit ctx: DialectInstanceContext): Option[String] = {
mapping.ifMapping.option()
.flatMap(ifMappingId => choose(map, ifMappingId, mapping))
}

def choose(map: YMap, ifMappingId: String, mapping: AnyMapping)(implicit ctx: DialectInstanceContext): Option[String] = {
ctx.findNodeMapping(ifMappingId).flatMap { ifMapping =>
val (ifParsedNode, conformsParsing) = couldParse(map, ifMapping)
if (!conformsParsing) return mapping.elseMapping.option()
else {
val report = validateParsed(ifMapping, ifParsedNode)
val isIfCompliant = report.conforms
if (isIfCompliant) return mapping.thenMapping.option()
else return mapping.elseMapping.option()
}
}
}
private def couldParse(map: YMap, ifMapping: ctx.NodeMappable) = {
val nextContext = ctx.copy(DefaultErrorHandler())
val ifParsed = parse("", "if", map, ifMapping, Map.empty)(nextContext)
val conforms = ignoreClosedShapeErrors(nextContext.eh.getResults).isEmpty
(ifParsed, conforms)
}

private def ignoreClosedShapeErrors(results: Seq[AMFValidationResult]): Seq[AMFValidationResult] = {
results.filterNot(_.validationId.contains("closed"))
}

private def validateParsed(ifMapping: ctx.NodeMappable, ifParsed: DialectDomainElement) = {
val mappingsInTree = AmlSubGraphCollector.collect(ifMapping.id, ctx.dialect)
val validator = new CustomShaclValidator(Map.empty, AMFStyle)
val validations = ctx.constraints
.map(p => p.validations.filter(x => x.targetClass.intersect(mappingsInTree).nonEmpty))
.getOrElse(Nil)
val report = validator.validate(ifParsed, validations)
report
}
}

private def parseConditionally(path: String,
defaultId: String,
astMap: YMap,
mappable: AnyMapping): DialectDomainElement = {
IfThenElseBranchCriteria
.choose(astMap, mappable)
.flatMap(ctx.findNodeMapping)
.map(mapping => parse(path, defaultId, astMap, mapping, Map.empty)) match {
case Some(mapping) => mapping
case None =>
// TODO: Violation
DialectDomainElement().withId(defaultId)
}
}

private def parseWithNodeMapping(defaultId: String,
astMap: YMap,
ast: YNode,
Expand All @@ -181,28 +150,31 @@ case class InstanceNodeParser(root: Root)(implicit ctx: DialectInstanceContext)
givenAnnotations: Option[Annotations],
additionalKey: Option[String],
mapping: NodeMapping)(implicit ctx: DialectInstanceContext) = {
val annotations = givenAnnotations.getOrElse(Annotations(ast))
val annotations = givenAnnotations.getOrElse(Annotations(ast))
val node: DialectDomainElement = DialectDomainElement(defaultId.urlDecoded, mapping, annotations)
val finalId = generateNodeId(node, astMap, Seq(defaultId), defaultId, mapping, additionalProperties, rootNode, root)
val finalId =
generateNodeId(node, astMap, Seq(defaultId), defaultId, mapping, additionalProperties, rootNode, root)
node.withId(finalId)
parseAnnotations(astMap, node, ctx.declarations)
mapping.propertiesMapping().foreach { propertyMapping =>
val propertyName = propertyMapping.name().value()
astMap.key(propertyName) match {
case Some(entry) =>
val nestedId =
if (Option(ctx.dialect.documents()).flatMap(_.selfEncoded().option()).getOrElse(false) && rootNode)
defaultId + "#/"
else defaultId
parseProperty(nestedId, entry, propertyMapping, node)
case None => // ignore
astMap.key(propertyName).foreach { entry =>
val nestedId = computeNestedPropertyId(defaultId, rootNode, ctx)
parseProperty(nestedId, entry, propertyMapping, node)
}
}
val shouldErrorOnExtraProperties = mapping.closed.option().getOrElse(true) // default behaviour is to error out
if (shouldErrorOnExtraProperties) checkNodeForAdditionalKeys(finalId, mapping.id, astMap.map, mapping, astMap, rootNode, additionalKey)
if (shouldErrorOnExtraProperties)
checkNodeForAdditionalKeys(finalId, mapping.id, astMap.map, mapping, astMap, rootNode, additionalKey)
node
}

private def computeNestedPropertyId(defaultId: String, rootNode: Boolean, ctx: DialectInstanceContext) = {
if (Option(ctx.dialect.documents()).flatMap(_.selfEncoded().option()).getOrElse(false) && rootNode)
defaultId + "#/"
else defaultId
}

private def parseProperty(id: String,
propertyEntry: YMapEntry,
property: PropertyMapping,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import scala.language.higherKinds

object ObjectCollectionPropertyParser extends NodeMappableHelper {

type NodeParser = (String, String, YNode, NodeMappable, Map[String, Any]) => DialectDomainElement
type NodeParser = (String, String, YNode, NodeMappable, Map[String, Any], Boolean) => DialectDomainElement

type ObjectUnionParser[T <: DomainElement] =
(String, Seq[String], YNode, NodeWithDiscriminator[_], Map[String, Any]) => DialectDomainElement
Expand Down Expand Up @@ -51,7 +51,7 @@ object ObjectCollectionPropertyParser extends NodeMappableHelper {
ctx.dialect.declares.find(_.id == range.head) match {
case Some(nodeMapping: NodeMappable) =>
val dialectDomainElement =
nodeParser(id, nestedObjectId, elementNode, nodeMapping, additionalProperties)
nodeParser(id, nestedObjectId, elementNode, nodeMapping, additionalProperties, false)
checkDuplicated(dialectDomainElement, elementNode, idsMap)
Some(dialectDomainElement)
case _ => None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ object ObjectMapPropertyParser extends NodeMappableHelper {
case range: Seq[String] if range.size == 1 =>
ctx.dialect.declares.find(_.id == range.head) match {
case Some(nodeMapping: NodeMappable) if keyEntry.value.tagType != YType.Null =>
Some(nodeParser(id, nestedObjectId, keyEntry.value, nodeMapping, keyAdditionalProperties))
Some(nodeParser(id, nestedObjectId, keyEntry.value, nodeMapping, keyAdditionalProperties, false))
case _ => None
}
case _ => None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ object SimpleObjectPropertyParser {
ctx.dialect.declares.find(_.id == rangeId) match {
case Some(nodeMapping: NodeMappable) =>
val dialectDomainElement =
nodeParser(id, nestedObjectId, propertyEntry.value, nodeMapping, additionalProperties)
nodeParser(id, nestedObjectId, propertyEntry.value, nodeMapping, additionalProperties, false)
node.withObjectField(property, dialectDomainElement, Right(propertyEntry))
case _ => // ignore
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package amf.aml.internal.parse.instances.parser.applicable

import amf.aml.client.scala.model.domain.AnyMapping
import amf.aml.internal.parse.instances.DialectInstanceContext
import ApplicableMapping.aggregate
import amf.aml.internal.render.emitters.instances.DialectIndex
import amf.core.internal.parser.Root
import org.yaml.model.YMap

object AndFinderPath extends FinderPath {
override def walk(mapping: AnyMapping, map: YMap, index: DialectIndex, finder: ApplicableMappingFinder)(
implicit ctx: DialectInstanceContext): ApplicableMapping = {
if (mapping.and.isEmpty) return ApplicableMapping.empty
val andMappings = findMappingsFor(mapping.and, index)
val aggregateMapping = aggregate(andMappings.map(node => finder.find(map, node, index)))
aggregateMapping
}
}
Loading