Skip to content

Commit

Permalink
Merge pull request #1466 from znsio/html-updated
Browse files Browse the repository at this point in the history
GUI editor integration and Amber validation color for missing optional fields
  • Loading branch information
joelrosario authored Dec 10, 2024
2 parents a14f07d + 98dec09 commit a8cef12
Show file tree
Hide file tree
Showing 24 changed files with 986 additions and 133 deletions.
8 changes: 5 additions & 3 deletions application/src/main/kotlin/application/ExamplesCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,11 @@ For example:
if (contractFile != null && !contractFile!!.exists())
exitWithMessage("Could not find file ${contractFile!!.path}")

val host = "0.0.0.0"
val port = 9001
server = ExamplesInteractiveServer(
"0.0.0.0",
9001,
host,
port,
testBaseURL,
contractFile,
filterName,
Expand All @@ -517,7 +519,7 @@ For example:
)
addShutdownHook()

consoleLog(StringLog("Examples Interactive server is running on http://0.0.0.0:9001/_specmatic/examples. Ctrl + C to stop."))
consoleLog(StringLog("Examples Interactive server is running on ${consolePrintableURL(host, port)}/_specmatic/examples. Ctrl + C to stop."))
while (true) sleep(10000)
} catch (e: Exception) {
logger.log(exceptionCauseMessage(e))
Expand Down
4 changes: 2 additions & 2 deletions application/src/main/kotlin/application/HTTPStubEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.specmatic.core.WorkingDirectory
import io.specmatic.core.log.NewLineLogMessage
import io.specmatic.core.log.StringLog
import io.specmatic.core.log.consoleLog
import io.specmatic.core.utilities.consolePrintableURL
import io.specmatic.mock.ScenarioStub
import io.specmatic.stub.HttpClientFactory
import io.specmatic.stub.HttpStub
Expand Down Expand Up @@ -46,8 +47,7 @@ class HTTPStubEngine {
timeoutMillis = gracefulRestartTimeoutInMs
).also {
consoleLog(NewLineLogMessage)
val protocol = if (keyStoreData != null) "https" else "http"
consoleLog(StringLog("Stub server is running on ${protocol}://$host:$port. Ctrl + C to stop."))
consoleLog(StringLog("Stub server is running on ${consolePrintableURL(host, port, keyStoreData)}. Ctrl + C to stop."))
}
}
}
5 changes: 2 additions & 3 deletions application/src/main/kotlin/application/ProxyCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.specmatic.core.Configuration.Companion.DEFAULT_PROXY_HOST
import io.specmatic.core.Configuration.Companion.DEFAULT_PROXY_PORT
import io.specmatic.core.DEFAULT_TIMEOUT_IN_MILLISECONDS
import io.specmatic.core.log.*
import io.specmatic.core.utilities.consolePrintableURL
import io.specmatic.core.utilities.exceptionCauseMessage
import io.specmatic.core.utilities.exitWithMessage
import io.specmatic.proxy.Proxy
Expand Down Expand Up @@ -64,9 +65,7 @@ class ProxyCommand : Callable<Unit> {
proxy = Proxy(host, port, targetBaseURL, proxySpecmaticDataDir, keyStoreData, timeoutInMs)
addShutdownHook()

val protocol = keyStoreData?.let { "https" } ?: "http"

consoleLog(StringLog("Proxy server is running on $protocol://$host:$port. Ctrl + C to stop."))
consoleLog(StringLog("Proxy server is running on ${consolePrintableURL(host, port, keyStoreData)}. Ctrl + C to stop."))
while(true) sleep(10000)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import io.specmatic.core.Feature
import io.specmatic.core.log.StringLog
import io.specmatic.core.log.consoleLog
import io.specmatic.core.log.logger
import io.specmatic.core.utilities.ContractPathData
import io.specmatic.core.utilities.contractFilePathsFrom
import io.specmatic.core.utilities.contractStubPaths
import io.specmatic.core.utilities.exitIfAnyDoNotExist
import io.specmatic.core.utilities.*
import io.specmatic.mock.ScenarioStub
import io.specmatic.stub.stateful.StatefulHttpStub
import picocli.CommandLine.Command
Expand Down Expand Up @@ -98,7 +95,7 @@ class VirtualServiceCommand : Callable<Int> {
Configuration.configFilePath,
stubData.flatMap { it.second }.also { it.logExamplesCachedAsSeedData() }
)
logger.log("Virtual service started on http://$host:$port")
logger.log("Virtual service is running on ${consolePrintableURL(host, port)}. Ctrl + C to stop.")
latch.await()
}

Expand Down
3 changes: 3 additions & 0 deletions core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-reflect:1.9.24"
implementation "org.eclipse.jgit:org.eclipse.jgit:$jgit_version"
implementation "org.eclipse.jgit:org.eclipse.jgit.ssh.apache:$jgit_version"
implementation 'com.jayway.jsonpath:json-path:2.8.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.15.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0'

implementation 'com.flipkart.zjsonpatch:zjsonpatch:0.4.16'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,18 @@ class OpenApiSpecification(
return OpenAPIV3Parser().read(openApiFilePath, null, resolveExternalReferences()) != null
}

fun getImplicitOverlayContent(openApiFilePath: String): String {
return File(openApiFilePath).let { openApiFile ->
if(!openApiFile.isFile)
return@let ""

val overlayFile = openApiFile.canonicalFile.parentFile.resolve(openApiFile.nameWithoutExtension + "_overlay.yaml")
if(overlayFile.isFile) return@let overlayFile.readText()

return@let ""
}
}

fun fromYAML(
yamlContent: String,
openApiFilePath: String,
Expand All @@ -120,16 +132,7 @@ class OpenApiSpecification(
specmaticConfig: SpecmaticConfig = SpecmaticConfig(),
overlayContent: String = ""
): OpenApiSpecification {
val implicitOverlayFile = File(openApiFilePath).let { openApiFile ->
if(!openApiFile.isFile)
return@let ""

val overlayFile = openApiFile.canonicalFile.parentFile.resolve(openApiFile.nameWithoutExtension + "_overlay.yaml")
if(overlayFile.isFile)
return@let overlayFile.readText()

return@let ""
}
val implicitOverlayFile = getImplicitOverlayContent(openApiFilePath)

val parseResult: SwaggerParseResult =
OpenAPIV3Parser().readContents(
Expand Down Expand Up @@ -205,7 +208,7 @@ class OpenApiSpecification(

private fun resolveExternalReferences(): ParseOptions = ParseOptions().also { it.isResolve = true }

private fun String.applyOverlay(overlayContent: String): String {
fun String.applyOverlay(overlayContent: String): String {
if(overlayContent.isBlank())
return this

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/kotlin/io/specmatic/core/Feature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,8 @@ data class Feature(
} != null
}

fun matchResultSchemaFlagBased(primaryPatternName: String?, secondaryPatternName: String, value: Value): Result {
val updatedResolver = flagsBased.update(scenarios.last().resolver)
fun matchResultSchemaFlagBased(primaryPatternName: String?, secondaryPatternName: String, value: Value, mismatchMessages: MismatchMessages): Result {
val updatedResolver = flagsBased.update(scenarios.last().resolver).copy(mismatchMessages = mismatchMessages)
return try {
val pattern = primaryPatternName ?: secondaryPatternName
val resolvedPattern = updatedResolver.getPattern(withPatternDelimiters(pattern))
Expand Down
9 changes: 9 additions & 0 deletions core/src/main/kotlin/io/specmatic/core/KeyError.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ sealed class KeyError {
abstract val name: String

abstract fun missingKeyToResult(keyLabel: String, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure

abstract fun missingOptionalKeyToResult(keyLabel: String, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure
}

data class MissingKeyError(override val name: String) : KeyError() {
override fun missingKeyToResult(keyLabel: String, mismatchMessages: MismatchMessages): Failure =
Failure(mismatchMessages.expectedKeyWasMissing(keyLabel, name))

override fun missingOptionalKeyToResult(keyLabel: String, mismatchMessages: MismatchMessages): Failure {
return Failure(mismatchMessages.optionalKeyMissing(keyLabel, name), isPartial = true)
}
}

data class UnexpectedKeyError(override val name: String) : KeyError() {
override fun missingKeyToResult(keyLabel: String, mismatchMessages: MismatchMessages): Failure =
Failure(mismatchMessages.unexpectedKey(keyLabel, name))

override fun missingOptionalKeyToResult(keyLabel: String, mismatchMessages: MismatchMessages): Failure =
Failure(mismatchMessages.unexpectedKey(keyLabel, name))
}
20 changes: 16 additions & 4 deletions core/src/main/kotlin/io/specmatic/core/Result.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ sealed class Result {
abstract fun partialSuccess(message: String): Result
abstract fun isPartialSuccess(): Boolean

abstract fun isPartialFailure(): Boolean

abstract fun testResult(): TestResult
abstract fun withFailureReason(urlPathMisMatch: FailureReason): Result
abstract fun throwOnFailure(): Success
Expand Down Expand Up @@ -111,14 +113,14 @@ sealed class Result {
}
}

data class Failure(val causes: List<FailureCause> = emptyList(), val breadCrumb: String = "", val failureReason: FailureReason? = null) : Result() {
constructor(message: String="", cause: Failure? = null, breadCrumb: String = "", failureReason: FailureReason? = null): this(listOf(FailureCause(message, cause)), breadCrumb, failureReason)
data class Failure(val causes: List<FailureCause> = emptyList(), val breadCrumb: String = "", val failureReason: FailureReason? = null, val isPartial: Boolean = false) : Result() {
constructor(message: String="", cause: Failure? = null, breadCrumb: String = "", failureReason: FailureReason? = null, isPartial: Boolean? = false): this(listOf(FailureCause(message, cause)), breadCrumb, failureReason, isPartial ?: false)

companion object {
fun fromFailures(failures: List<Failure>): Failure {
return Failure(failures.map {
it.toFailureCause()
})
}, isPartial = failures.all { it.isPartial })
}
}

Expand All @@ -135,6 +137,7 @@ sealed class Result {
.plus("$prefix$breadCrumb")
}


override fun ifSuccess(function: () -> Result) = this
override fun withBindings(bindings: Map<String, String>, response: HttpResponse): Result {
return this
Expand All @@ -149,6 +152,7 @@ sealed class Result {
}

override fun isPartialSuccess(): Boolean = false
override fun isPartialFailure(): Boolean = isPartial
override fun testResult(): TestResult {
if(shouldBeIgnored())
return TestResult.Error
Expand All @@ -169,7 +173,7 @@ sealed class Result {
}

fun reason(errorMessage: String) = Failure(errorMessage, this)
override fun breadCrumb(breadCrumb: String) = Failure(cause = this, breadCrumb = breadCrumb)
override fun breadCrumb(breadCrumb: String) = Failure(cause = this, breadCrumb = breadCrumb, isPartial = isPartial)
override fun failureReason(failureReason: FailureReason?): Result {
return this.copy(failureReason = failureReason)
}
Expand Down Expand Up @@ -285,6 +289,7 @@ sealed class Result {
}

override fun isPartialSuccess(): Boolean = partialSuccessMessage != null
override fun isPartialFailure(): Boolean = false
override fun testResult(): TestResult {
return TestResult.Success
}
Expand Down Expand Up @@ -333,6 +338,9 @@ interface MismatchMessages {
fun mismatchMessage(expected: String, actual: String): String
fun unexpectedKey(keyLabel: String, keyName: String): String
fun expectedKeyWasMissing(keyLabel: String, keyName: String): String
fun optionalKeyMissing(keyLabel: String, keyName: String): String {
return expectedKeyWasMissing("Optional ${keyLabel.capitalizeFirstChar()}", keyName)
}
fun valueMismatchFailure(expected: String, actual: Value?, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure {
return mismatchResult(expected, valueError(actual) ?: "null", mismatchMessages)
}
Expand All @@ -350,6 +358,10 @@ object DefaultMismatchMessages: MismatchMessages {
override fun expectedKeyWasMissing(keyLabel: String, keyName: String): String {
return "Expected ${keyLabel.lowercase()} named \"$keyName\" was missing"
}

override fun optionalKeyMissing(keyLabel: String, keyName: String): String {
return "Expected Optional ${keyLabel.lowercase()} named \"$keyName\" was missing"
}
}

fun mismatchResult(expected: String, actual: String, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure = Failure(mismatchMessages.mismatchMessage(expected, actual))
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/io/specmatic/core/Results.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class Results(val results: List<Result> = emptyList()) {
}

fun toResultIfAny(): Result {
return results.find { it is Result.Success } ?: Result.Failure(results.joinToString("\n\n") { it.toReport().toText() })
return results.find { it is Result.Success } ?: Result.Failure(results.joinToString("\n\n") { it.toReport().toText() }, isPartial = results.all { it.isPartialFailure() })
}

val failureCount
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@

import com.fasterxml.jackson.core.JsonLocation
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.*
import com.fasterxml.jackson.databind.util.RawValue
import java.math.BigDecimal
import java.math.BigInteger
import java.util.AbstractMap.SimpleEntry

class CustomJsonNodeFactory(
nodeFactory: JsonNodeFactory,
private val parserFactory: CustomParserFactory
) : JsonNodeFactory() {
private val delegate: JsonNodeFactory = nodeFactory

/*
* "Why isn't this a map?" you might be wondering. Well, when the nodes are created, they're all
* empty and a node's hashCode is based on its children. So if you use a map and put the node
* in, then the node's hashCode is based on no children, then when you lookup your node, it is
* *with* children, so the hashcodes are different. Instead of all of this, you have to iterate
* through a listing and find their matches once the objects have been populated, which is only
* after the document has been completely parsed
*/
private val locationMapping: MutableList<Pair<JsonNode, JsonLocation>> = mutableListOf()

/**
* Given a node, find its location, or null if it wasn't found
*
* @param jsonNode the node to search for
* @return the location of the node or null if not found
*/
fun getLocationForNode(jsonNode: JsonNode?): JsonLocation? {
return locationMapping.filter { e: Pair<JsonNode, JsonLocation> -> e.first.equals(jsonNode) }
.map { e: Pair<JsonNode, JsonLocation> -> e.second }.firstOrNull()
}

/**
* Simple interceptor to mark the node in the lookup list and return it back
*
* @param <T> the type of the JsonNode
* @param node the node itself
* @return the node itself, having marked its location
</T> */
private fun <T : JsonNode?> markNode(node: T?): T {
val loc: JsonLocation = parserFactory.getParser()!!.currentLocation
locationMapping.add(node!! to loc)
return node
}

public override fun booleanNode(v: Boolean): BooleanNode {
return markNode(delegate.booleanNode(v))
}

public override fun nullNode(): NullNode {
return markNode(delegate.nullNode())
}

public override fun numberNode(value: Byte?): ValueNode {
return markNode(delegate.numberNode(value))
}

public override fun missingNode(): JsonNode {
return super.missingNode()
}

public override fun numberNode(value: Short?): ValueNode {
return markNode(delegate.numberNode(value))
}

public override fun numberNode(v: Int): NumericNode {
return markNode(delegate.numberNode(v))
}

public override fun numberNode(value: Long): NumericNode {
return markNode(delegate.numberNode(value))
}

public override fun numberNode(v: BigInteger): ValueNode {
return markNode(delegate.numberNode(v))
}

public override fun numberNode(value: Float): NumericNode {
return markNode(delegate.numberNode(value))
}

public override fun numberNode(value: Double): NumericNode {
return markNode(delegate.numberNode(value))
}

public override fun numberNode(v: BigDecimal): ValueNode {
return markNode(delegate.numberNode(v))
}

public override fun textNode(text: String?): TextNode {
return markNode(delegate.textNode(text))
}

public override fun binaryNode(data: ByteArray?): BinaryNode {
return markNode(delegate.binaryNode(data))
}

public override fun binaryNode(data: ByteArray?, offset: Int, length: Int): BinaryNode {
return markNode(delegate.binaryNode(data, offset, length))
}

public override fun pojoNode(pojo: Any?): ValueNode {
return markNode(delegate.pojoNode(pojo))
}

public override fun rawValueNode(value: RawValue?): ValueNode {
return markNode(delegate.rawValueNode(value))
}

public override fun arrayNode(): ArrayNode {
return markNode(delegate.arrayNode())
}

public override fun arrayNode(capacity: Int): ArrayNode {
return markNode(delegate.arrayNode(capacity))
}

public override fun objectNode(): ObjectNode {
return markNode(delegate.objectNode())
}

companion object {
private const val serialVersionUID = 8807395553661461181L
}
}
Loading

0 comments on commit a8cef12

Please sign in to comment.