Skip to content

Commit

Permalink
#102 Add requirements level checker to Asciidoc tooling. Fail process…
Browse files Browse the repository at this point in the history
… in case of an error.
  • Loading branch information
d-gregorczyk committed Nov 1, 2022
1 parent 7cfce5f commit 84b920f
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 13 deletions.
Expand Up @@ -5,6 +5,7 @@ import org.asciidoctor.Options
import org.asciidoctor.SafeMode
import org.sdpi.asciidoc.extension.DisableSectNumsProcessor
import org.sdpi.asciidoc.extension.NumberingProcessor
import org.sdpi.asciidoc.extension.RequirementLevelProcessor
import org.sdpi.asciidoc.extension.RequirementsBlockProcessor
import java.io.File
import java.io.OutputStream
Expand All @@ -30,6 +31,7 @@ class AsciidocConverter(
else -> null
}
))
asciidoctor.javaExtensionRegistry().treeprocessor(RequirementLevelProcessor())
asciidoctor.javaExtensionRegistry().preprocessor(DisableSectNumsProcessor())

asciidoctor.requireLibrary("asciidoctor-diagram") // enables plantuml
Expand Down
Expand Up @@ -16,6 +16,7 @@ import org.sdpi.asciidoc.extension.RequirementsBlockProcessor
import org.sdpi.asciidoc.extension.NumberingProcessor
import java.io.File
import java.io.FileOutputStream
import kotlin.system.exitProcess

fun main(args: Array<String>) = ConvertAndVerifySupplement().main(
when (System.getenv().containsKey("CI")) {
Expand Down Expand Up @@ -65,6 +66,7 @@ class ConvertAndVerifySupplement : CliktCommand("convert-supplement") {
}.onFailure {
logger.error { it.message }
logger.trace(it) { it.message }
exitProcess(1)
}
}
}
Expand Down
Expand Up @@ -54,4 +54,13 @@ fun validate(value: Boolean, node: StructuralNode, msg: () -> String) {
fun StructuralNode.isAppendix() = when (val section = this.toSealed()) {
is StructuralNodeWrapper.Section -> section.wrapped.sectionName == "appendix"
else -> false
}
}

/**
* Takes a string and converts it to a [RequirementLevel] enum.
*
* @param raw Raw text being shall, should or may.
*
* @return the [RequirementLevel] enum or null if the conversion failed (raw was not shall, should or may).
*/
fun resolveRequirementLevel(raw: String) = RequirementLevel.values().firstOrNull { it.keyword == raw }
@@ -0,0 +1,103 @@
package org.sdpi.asciidoc.extension

import org.apache.logging.log4j.kotlin.Logging
import org.asciidoctor.ast.Block
import org.asciidoctor.ast.Document
import org.asciidoctor.ast.StructuralNode
import org.asciidoctor.extension.Treeprocessor
import org.sdpi.asciidoc.*
import org.sdpi.asciidoc.model.RequirementLevel
import org.sdpi.asciidoc.model.StructuralNodeWrapper
import org.sdpi.asciidoc.model.toSealed

/**
* Checks expected requirement levels in SDPi requirements.
*
* Loops all paragraphs of sdpi_requirement blocks and checks if there is exactly one requirement level keyword that
* equals the value of the attribute key sdpi_req_level.
*
* Exits with an error if
*
* - there are multiple different keywords
* - if no keyword is found
* - if more than one occurrence of the keyword is found
*/
class RequirementLevelProcessor : Treeprocessor() {
override fun process(document: Document): Document {
processBlock(document as StructuralNode)
return document
}

private fun processBlock(block: StructuralNode) {
block.toSealed().let { node ->
when (node) {
is StructuralNodeWrapper.SdpiRequirement -> {
processRequirement(node.wrapped)
}
else -> logger.debug { "Ignore block of type '${block.context}'" }
}
}

block.blocks.forEach { processBlock(it) }
}

private fun processRequirement(block: Block) {
val attributes = Attributes(block.attributes)
val levelRaw = attributes[BlockAttribute.REQUIREMENT_LEVEL]
checkNotNull(levelRaw) {
("Missing requirement level for SDPi requirement with id ${attributes[BlockAttribute.ID]}").also {
logger.error { it }
}
}
val level = resolveRequirementLevel(levelRaw)
checkNotNull(level) {
("Invalid requirement level for SDPi requirement with id ${attributes[BlockAttribute.ID]}: $level").also {
logger.error { it }
}
}

val msgPrefix = "Check requirement level for #${attributes[BlockAttribute.ID]} (${level.keyword}):"
var levelCount = 0
block.blocks.forEach { reqBlock ->
reqBlock.toSealed().let { childNode ->
when (childNode) {
is StructuralNodeWrapper.Paragraph -> {
levelCount += childNode.wrapped.source
.split(" ")
.map { it.trim() }
.count { it == level.keyword }

RequirementLevel.values().filter { it != level }.forEach { notLevel ->
val notLevelCount = childNode.wrapped.source
.split(" ")
.map { it.trim() }
.count { it == notLevel.keyword }
check(notLevelCount == 0) {
("$msgPrefix Requirement level keyword '${notLevel.keyword}' found").also {
logger.error { it }
}
}
}
}
else -> Unit
}
}
}

check(levelCount > 0) {
("$msgPrefix Requirement level keyword is missing").also {
logger.error { it }
}
}

check(levelCount == 1) {
("$msgPrefix Only one requirement level keyword allowed per requirement").also {
logger.error { it }
}
}

logger.info { "$msgPrefix Done" }
}

private companion object : Logging
}
Expand Up @@ -27,7 +27,7 @@ class RequirementsBlockProcessor : BlockProcessor(BLOCK_NAME_SDPI_REQUIREMENT) {
private companion object : Logging {
val REQUIREMENT_NUMBER_FORMAT = "^r(\\d+)$".toRegex()
val REQUIREMENT_TITLE_FORMAT = "^([A-Z])*?R(\\d+)$".toRegex()
val REQUIREMENT_ROLE = "requirement"
const val REQUIREMENT_ROLE = "requirement"
}

private val detectedRequirements = mutableMapOf<Int, SdpiRequirement>()
Expand All @@ -43,7 +43,7 @@ class RequirementsBlockProcessor : BlockProcessor(BLOCK_NAME_SDPI_REQUIREMENT) {
parent: StructuralNode, reader: Reader,
attributes: MutableMap<String, Any>
): Any = retrieveRequirement(reader, Attributes(attributes)).let { requirement ->
logger.info { "Found SDPi requirement #{${requirement.number}: $requirement" }
logger.info { "Found SDPi requirement #${requirement.number}: $requirement" }
requirement.asciiDocAttributes[BlockAttribute.ROLE] = REQUIREMENT_ROLE
storeRequirement(requirement)
createBlock(
Expand All @@ -63,11 +63,9 @@ class RequirementsBlockProcessor : BlockProcessor(BLOCK_NAME_SDPI_REQUIREMENT) {
val requirementNumber = matchResults.map { it.groupValues[1] }.toList().first().toInt()
val lines = reader.readLines()
val requirementLevel =
RequirementLevel.values().firstOrNull { it.keyword == attributes[BlockAttribute.REQUIREMENT_LEVEL] }.let {
checkNotNull(it) {
("Missing requirement level for SDPi requirement #$requirementNumber").also {
logger.error { it }
}
checkNotNull(resolveRequirementLevel(attributes[BlockAttribute.REQUIREMENT_LEVEL] ?: "")) {
("Missing requirement level for SDPi requirement #$requirementNumber").also {
logger.error { it }
}
}
return SdpiRequirement(
Expand Down
@@ -1,8 +1,10 @@
package org.sdpi.asciidoc.model

import org.asciidoctor.ast.Block
import org.asciidoctor.ast.Document
import org.asciidoctor.ast.Section
import org.asciidoctor.ast.StructuralNode
import org.sdpi.asciidoc.extension.BLOCK_NAME_SDPI_REQUIREMENT

/**
* Creates a [StructuralNodeWrapper] from a structural node.
Expand All @@ -12,16 +14,25 @@ fun StructuralNode.toSealed(): StructuralNodeWrapper {
return when (this.context) {
"section" -> StructuralNodeWrapper.Section(this as Section)
"document" -> StructuralNodeWrapper.Document(this as Document)
"paragraph" -> StructuralNodeWrapper.Paragraph(this as Block)
"sidebar" -> this.attributes.entries.find {
it.key == "1" && it.value == BLOCK_NAME_SDPI_REQUIREMENT
}?.let {
StructuralNodeWrapper.SdpiRequirement(this as Block)
} ?: StructuralNodeWrapper.Sidebar(this as Block)
else -> StructuralNodeWrapper.Unknown
}
}

/**
* Wrapper class for improved functional dispatching.
*/
sealed class StructuralNodeWrapper() {
sealed class StructuralNodeWrapper {
data class Section(val wrapped: org.asciidoctor.ast.Section) : StructuralNodeWrapper()
data class Document(val wrapped: org.asciidoctor.ast.Document) : StructuralNodeWrapper()
data class Sidebar(val wrapped: Block) : StructuralNodeWrapper()
data class SdpiRequirement(val wrapped: Block) : StructuralNodeWrapper()
data class Paragraph(val wrapped: Block): StructuralNodeWrapper()
object Unknown : StructuralNodeWrapper()
}

Expand Up @@ -7,8 +7,8 @@

===== Encoding of Production Specifications

.R0009
[sdpi_requirement#r0009,sdpi_req_level=shall]
.R0010
[sdpi_requirement#r0010,sdpi_req_level=shall]
****
If a <<term_manufacturer>> of a <<actor_somds_provider>> intends to include MDS production specifications in the WS-Discovery Scopes of the <<actor_somds_provider>>, the <<actor_somds_provider>> shall encode the production specifications by using the rules in <<vol2_listing_encoding_production_specification_mds>>.
Expand Down Expand Up @@ -65,8 +65,8 @@ NOTE: `ProductionSpecification` is specified in <<vol2_listing_encoding_producti

===== Encoding of Attributes

.R0010
[sdpi_requirement#r0010,sdpi_req_level=shall]
.R0011
[sdpi_requirement#r0011,sdpi_req_level=shall]
****
If a <<term_manufacturer>> of a <<actor_somds_provider>> intends to include MDS attributes in the WS-Discovery Scopes of the <<actor_somds_provider>>, the <<actor_somds_provider>> shall encode the attributes by using the rules in <<vol2_listing_encoding_attribute_mds>>.
Expand Down

0 comments on commit 84b920f

Please sign in to comment.