Skip to content

Commit

Permalink
Add StatefulHttpStub and virtual-service command to start a stateful …
Browse files Browse the repository at this point in the history
…stub server
  • Loading branch information
yogeshnikam671 committed Nov 15, 2024
1 parent 4c6862f commit 6940bec
Show file tree
Hide file tree
Showing 14 changed files with 920 additions and 483 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import java.util.concurrent.Callable
ExamplesCommand::class,
SamplesCommand::class,
StubCommand::class,
VirtualServiceCommand::class,
SubscribeCommand::class,
TestCommand::class,
ValidateViaLogs::class,
Expand Down
116 changes: 116 additions & 0 deletions application/src/main/kotlin/application/VirtualServiceCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package application

import io.specmatic.core.Configuration
import io.specmatic.core.Configuration.Companion.DEFAULT_HTTP_STUB_HOST
import io.specmatic.core.Configuration.Companion.DEFAULT_HTTP_STUB_PORT
import io.specmatic.core.DEFAULT_WORKING_DIRECTORY
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.mock.ScenarioStub
import io.specmatic.stub.stateful.StatefulHttpStub
import picocli.CommandLine.Command
import picocli.CommandLine.Option
import java.util.concurrent.Callable
import java.util.concurrent.CountDownLatch

@Command(
name = "virtual-service",
mixinStandardHelpOptions = true,
description = ["Start a stateful virtual service with contract"]
)
class VirtualServiceCommand : Callable<Int> {

@Option(names = ["--host"], description = ["Host for the http stub"], defaultValue = DEFAULT_HTTP_STUB_HOST)
lateinit var host: String

@Option(names = ["--port"], description = ["Port for the http stub"], defaultValue = DEFAULT_HTTP_STUB_PORT)
var port: Int = 0

private val stubLoaderEngine = StubLoaderEngine()
private var server: StatefulHttpStub? = null
private val latch = CountDownLatch(1)

override fun call(): Int {
setup()

try {
startServer()
} catch(e: Exception) {
logger.log("An error occurred while starting the virtual service: ${e.message}")
return 1
}

return 0
}

private fun setup() {
exitIfContractPathsDoNotExist()
addShutdownHook()
}

private fun exitIfContractPathsDoNotExist() {
val contractPaths = contractStubPaths(Configuration.configFilePath).map { it.path }
exitIfAnyDoNotExist("The following specifications do not exist", contractPaths)
}

private fun addShutdownHook() {
Runtime.getRuntime().addShutdownHook(object : Thread() {
override fun run() {
try {
latch.countDown()
consoleLog(StringLog("Shutting down the virtual service"))
server?.close()
} catch (e: InterruptedException) {
currentThread().interrupt()
}
}
})
}

private fun stubContractPathData(): List<ContractPathData> {
return contractFilePathsFrom(Configuration.configFilePath, DEFAULT_WORKING_DIRECTORY) {
source -> source.stubContracts
}
}

private fun startServer() {
val stubData: List<Pair<Feature, List<ScenarioStub>>> = stubLoaderEngine.loadStubs(
stubContractPathData(),
emptyList(), // TODO - to be replaced with exampleDirs
Configuration.configFilePath,
false
)

server = StatefulHttpStub(
host,
port,
stubData.map { it.first },
Configuration.configFilePath
)
logger.log("Virtual service started on http://$host:$port")
latch.await()
}
}

















3 changes: 3 additions & 0 deletions core/src/main/kotlin/io/specmatic/core/ResponseBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import io.specmatic.core.value.Value
import io.specmatic.stub.RequestContext

class ResponseBuilder(val scenario: Scenario, val serverState: Map<String, Value>) {
val responseBodyPattern = scenario.httpResponsePattern.body
val resolver = scenario.resolver

fun build(requestContext: RequestContext): HttpResponse {
return scenario.generateHttpResponse(serverState, requestContext)
}
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/io/specmatic/core/SpecmaticConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ data class StubConfiguration(
val generative: Boolean? = false,
val delayInMilliseconds: Long? = getLongValue(SPECMATIC_STUB_DELAY),
val dictionary: String? = getStringValue(SPECMATIC_STUB_DICTIONARY),
val stateful: Boolean? = false
val includeMandatoryAndRequestedKeysInResponse: Boolean? = false
)

data class WorkflowIDOperation(
Expand Down
7 changes: 7 additions & 0 deletions core/src/main/kotlin/io/specmatic/core/pattern/Grammar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ internal fun withoutOptionality(key: String): String {
}
}

internal fun withOptionality(key: String): String {
return when {
key.endsWith(DEFAULT_OPTIONAL_SUFFIX) -> key
else -> "$key$DEFAULT_OPTIONAL_SUFFIX"
}
}

internal fun isOptional(key: String): Boolean =
key.endsWith(DEFAULT_OPTIONAL_SUFFIX) || key.endsWith(XML_ATTR_OPTIONAL_SUFFIX)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,14 @@ data class JSONObjectPattern(
}

override val typeName: String = "json object"

fun keysInNonOptionalFormat(): Set<String> {
return this.pattern.map { withoutOptionality(it.key) }.toSet()
}

fun patternForKey(key: String): Pattern? {
return pattern[withoutOptionality(key)] ?: pattern[withOptionality(key)]
}
}

fun generate(jsonPattern: Map<String, Pattern>, resolver: Resolver, typeAlias: String?): Map<String, Value> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ data class JSONObjectValue(val jsonObject: Map<String, Value> = emptyMap()) : Va

fun findFirstChildByName(name: String): Value? =
jsonObject[name]

fun keys() = jsonObject.keys

}

internal fun dictionaryToDeclarations(jsonObject: Map<String, Value>, types: Map<String, Pattern>, exampleDeclarations: ExampleDeclarations): Triple<Map<String, DeferredPattern>, Map<String, Pattern>, ExampleDeclarations> {
Expand Down
Loading

0 comments on commit 6940bec

Please sign in to comment.