Skip to content

Commit 7dffca5

Browse files
author
Robert Winkler
committed
Added WoT schemas
1 parent d7cf7dd commit 7dffca5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1816
-1278
lines changed

kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolServer.kt

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@ import ai.ancf.lmos.wot.Servient
44
import ai.ancf.lmos.wot.content.Content
55
import ai.ancf.lmos.wot.content.ContentCodecException
66
import ai.ancf.lmos.wot.content.ContentManager
7-
import ai.ancf.lmos.wot.thing.ExposedThing
8-
import ai.ancf.lmos.wot.thing.action.ExposedThingAction
7+
import ai.ancf.lmos.wot.thing.ExposedThingImpl
98
import ai.ancf.lmos.wot.thing.form.Form
109
import ai.ancf.lmos.wot.thing.form.Operation
11-
import ai.ancf.lmos.wot.thing.property.ExposedThingProperty
1210
import ai.ancf.lmos.wot.thing.property.ExposedThingProperty.*
13-
import ai.ancf.lmos.wot.thing.schema.*
1411
import ai.anfc.lmos.wot.binding.ProtocolServer
1512
import ai.anfc.lmos.wot.binding.ProtocolServerException
1613
import com.fasterxml.jackson.databind.DeserializationFeature
@@ -20,7 +17,6 @@ import io.ktor.serialization.jackson.*
2017
import io.ktor.server.application.*
2118
import io.ktor.server.engine.*
2219
import io.ktor.server.netty.*
23-
import io.ktor.server.plugins.*
2420
import io.ktor.server.plugins.contentnegotiation.*
2521
import io.ktor.server.request.*
2622
import io.ktor.server.routing.*
@@ -38,19 +34,20 @@ open class HttpProtocolServer(
3834
private val bindPort: Int = 8080,
3935
private val createServer: (host: String, port: Int, servient: Servient) -> EmbeddedServer<*, *> = ::defaultServer
4036
) : ProtocolServer {
41-
val things: MutableMap<String, ExposedThing> = mutableMapOf()
37+
val things: MutableMap<String, ExposedThingImpl> = mutableMapOf()
4238
var started = false
4339
private var server: EmbeddedServer<*, *>? = null
4440
private var actualAddresses: List<String> = listOf("http://$bindHost:$bindPort")
4541

4642
companion object {
4743
private val log = LoggerFactory.getLogger(HttpProtocolServer::class.java)
44+
private const val HTTP_METHOD_NAME = "htv:methodName"
4845
}
4946

5047
override suspend fun start(servient: Servient) {
5148
log.info("Starting on '{}' port '{}'", bindHost, bindPort)
52-
server = createServer(bindHost, bindPort, servient).start(wait)
5349
started = true
50+
server = createServer(bindHost, bindPort, servient).start(wait)
5451
}
5552

5653
// Stop the server
@@ -62,7 +59,7 @@ open class HttpProtocolServer(
6259
}
6360

6461
// Expose a thing
65-
override fun expose(thing: ExposedThing) {
62+
override fun expose(thing: ExposedThingImpl) {
6663
if (!started) throw ProtocolServerException("Server has not started yet")
6764

6865
log.info("Exposing thing '{}'", thing.id)
@@ -85,7 +82,7 @@ open class HttpProtocolServer(
8582
}
8683
}
8784

88-
internal fun exposeProperties(thing: ExposedThing, address: String, contentType: String) {
85+
internal fun exposeProperties(thing: ExposedThingImpl, address: String, contentType: String) {
8986
thing.properties.forEach { (name, property) ->
9087

9188
val href = getHrefWithVariablePattern(address, thing, "properties", name, property)
@@ -97,8 +94,22 @@ open class HttpProtocolServer(
9794
else -> listOf(Operation.READ_PROPERTY, Operation.WRITE_PROPERTY)
9895
}
9996

97+
// Determine the HTTP method based on property attributes
98+
val httpMethod = when {
99+
property.readOnly -> "GET"
100+
property.writeOnly -> "PUT"
101+
else -> null // No specific HTTP method for both read/write properties
102+
}
103+
100104
// Create the main form and add it to the property
101-
val form = Form(href = href, contentType = contentType, op = operations)
105+
val form = Form(
106+
href = href,
107+
contentType = contentType,
108+
op = operations,
109+
optionalProperties = hashMapOf<String, String>().apply {
110+
httpMethod?.let { put(HTTP_METHOD_NAME, it) }
111+
}
112+
)
102113
property.forms += form
103114
log.debug("Assign '{}' to Property '{}'", href, name)
104115

@@ -117,7 +128,7 @@ open class HttpProtocolServer(
117128
}
118129
}
119130

120-
internal fun exposeActions(thing: ExposedThing, address: String, contentType: String) {
131+
internal fun exposeActions(thing: ExposedThingImpl, address: String, contentType: String) {
121132
thing.actions.forEach { (name, action) ->
122133
val href: String = getHrefWithVariablePattern(address, thing, "actions", name, action)
123134
// Initialize the form using named parameters
@@ -133,7 +144,7 @@ open class HttpProtocolServer(
133144
}
134145
}
135146

136-
internal fun exposeEvents(thing: ExposedThing, address: String, contentType: String) {
147+
internal fun exposeEvents(thing: ExposedThingImpl, address: String, contentType: String) {
137148
thing.events.forEach { (name, event) ->
138149
val href = getHrefWithVariablePattern(address, thing, "events", name, event)
139150

@@ -153,7 +164,7 @@ open class HttpProtocolServer(
153164

154165
private fun getHrefWithVariablePattern(
155166
address: String,
156-
thing: ExposedThing,
167+
thing: ExposedThingImpl,
157168
type: String,
158169
interactionName: String,
159170
interaction: InteractionAffordance
@@ -167,7 +178,7 @@ open class HttpProtocolServer(
167178
}
168179

169180
// Destroy a thing
170-
override suspend fun destroy(thing: ExposedThing) {
181+
override suspend fun destroy(thing: ExposedThingImpl) {
171182
log.info("Removing thing '{}'", thing.id)
172183
things.remove(thing.id)
173184
}
@@ -186,15 +197,15 @@ fun Application.setupRouting(servient: Servient) {
186197
routing {
187198
route("/") {
188199
get {
189-
call.respond(servient.things.values.toList(), typeInfo<List<ExposedThing>>())
200+
call.respond(servient.things.values.toList(), typeInfo<List<ExposedThingImpl>>())
190201
}
191202
}
192203
route("/{id}") {
193204
get {
194205
val id = call.parameters["id"]
195-
val thing: ExposedThing? = servient.things[id]
206+
val thing: ExposedThingImpl? = servient.things[id]
196207
if (thing != null) {
197-
call.respond(thing, typeInfo<ExposedThing>())
208+
call.respond(thing, typeInfo<ExposedThingImpl>())
198209
} else {
199210
call.response.status(HttpStatusCode.NotFound)
200211
}

kotlin-wot-binding-http/src/main/kotlin/http/routes/AbstractInteractionRoute.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package ai.ancf.lmos.wot.binding.http.routes
22

33
import ai.ancf.lmos.wot.Servient
4-
import ai.ancf.lmos.wot.thing.ExposedThing
4+
import ai.ancf.lmos.wot.thing.ExposedThingImpl
55
import io.ktor.http.*
66
import io.ktor.server.request.*
77
import io.ktor.server.routing.*
88
import io.ktor.util.*
99

10-
internal abstract class AbstractInteractionRoute(private val servient: Servient, private val securityScheme: String, private val things: Map<String, ExposedThing>
10+
internal abstract class AbstractInteractionRoute(private val servient: Servient, private val securityScheme: String, private val things: Map<String, ExposedThingImpl>
1111
) : AbstractRoute() {
1212

1313
private fun checkCredentials(securityScheme: String?, id: String, request: RoutingRequest): Boolean {

kotlin-wot-binding-http/src/main/kotlin/http/routes/ThingsRoute.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ package ai.ancf.lmos.wot.binding.http.routes
22

33
import ai.ancf.lmos.wot.content.Content
44
import ai.ancf.lmos.wot.content.ContentManager
5-
import ai.ancf.lmos.wot.thing.ExposedThing
5+
import ai.ancf.lmos.wot.thing.ExposedThingImpl
66
import io.ktor.http.cio.*
77
import io.ktor.server.routing.*
88
import io.ktor.util.reflect.*
99

1010
/**
1111
* Endpoint for listing all Things from the [io.github.sanecity.wot.Servient].
1212
*/
13-
class ThingsRoute(private val things: Map<String, ExposedThing>) : AbstractRoute() {
13+
class ThingsRoute(private val things: Map<String, ExposedThingImpl>) : AbstractRoute() {
1414
@Throws(Exception::class)
1515
fun handle(request: RoutingRequest): Content {
1616
val requestContentType: String = getOrDefaultRequestContentType(request).toString()

kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolServerTest.kt

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
package ai.ancf.lmos.wot.binding.http
22

33
import ai.ancf.lmos.wot.Servient
4-
import ai.ancf.lmos.wot.thing.ExposedThing
5-
import ai.ancf.lmos.wot.thing.action.ActionHandler
6-
import ai.ancf.lmos.wot.thing.action.ExposedThingAction.ActionState
4+
import ai.ancf.lmos.wot.thing.ExposedThingImpl
75
import ai.ancf.lmos.wot.thing.exposedThing
86
import ai.ancf.lmos.wot.thing.form.Operation
97
import ai.ancf.lmos.wot.thing.form.Operation.READ_PROPERTY
108
import ai.ancf.lmos.wot.thing.form.Operation.WRITE_PROPERTY
11-
import ai.ancf.lmos.wot.thing.property.ExposedThingProperty.PropertyState
9+
import ai.ancf.lmos.wot.thing.schema.PropertyReadHandler
1210
import ai.ancf.lmos.wot.thing.schema.StringSchema
1311
import ai.ancf.lmos.wot.thing.schema.stringSchema
1412
import ai.anfc.lmos.wot.binding.ProtocolServerException
@@ -39,25 +37,24 @@ class HttpProtocolServerTest {
3937
private lateinit var server: HttpProtocolServer
4038
private val servient: Servient = mockk()
4139
private val mockServer: EmbeddedServer<*, *> = mockk()
42-
private val exposedThing: ExposedThing = exposedThing("test") {
43-
intProperty(PROPERTY_NAME, PropertyState(initialValue = 0, readHandler = { 2 }, writeHandler = { input -> input })){
44-
title = "title"
45-
}
46-
action<String, String>(ACTION_NAME, state = ActionState(handler = { input, _ ->
47-
input
48-
}))
49-
{
50-
title = ACTION_NAME
51-
input = stringSchema {
52-
title = "Action Input"
53-
minLength = 10
54-
default = "test"
55-
}
56-
output = StringSchema()
57-
}
58-
event<String, Nothing, Nothing>(EVENT_NAME){
59-
data = StringSchema()
40+
private val exposedThing: ExposedThingImpl = exposedThing(servient) {
41+
intProperty(PROPERTY_NAME) {
42+
observable = true
43+
readHandler = PropertyReadHandler { 2 }
44+
}
45+
action<String, String>(ACTION_NAME)
46+
{
47+
title = ACTION_NAME
48+
input = stringSchema {
49+
title = "Action Input"
50+
minLength = 10
51+
default = "test"
6052
}
53+
output = StringSchema()
54+
}
55+
event<String, Nothing, Nothing>(EVENT_NAME){
56+
data = StringSchema()
57+
}
6158
}
6259

6360
@BeforeTest
@@ -149,7 +146,7 @@ class HttpProtocolServerTest {
149146

150147
assertEquals(HttpStatusCode.OK, response.status)
151148

152-
val things : List<ExposedThing> = response.body()
149+
val things : List<ExposedThingImpl> = response.body()
153150
assertEquals(1, things.size)
154151

155152

@@ -167,7 +164,7 @@ class HttpProtocolServerTest {
167164
// Perform GET request on "/test"
168165
val response = client.get("/${exposedThing.id}")
169166

170-
val thing : ExposedThing = response.body()
167+
val thing : ExposedThingImpl = response.body()
171168

172169
assertEquals(HttpStatusCode.OK, response.status)
173170
assertEquals(exposedThing.id, thing.id)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
dependencies {
22
api(project(":kotlin-wot"))
33
api(project(":kotlin-wot-binding-http"))
4+
implementation("ch.qos.logback:logback-classic:1.5.12")
45
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package integration
2+
3+
import ai.ancf.lmos.wot.Servient
4+
import ai.ancf.lmos.wot.Wot
5+
import ai.ancf.lmos.wot.binding.http.HttpProtocolClientFactory
6+
import ai.ancf.lmos.wot.binding.http.HttpProtocolServer
7+
import kotlinx.coroutines.Dispatchers
8+
import kotlinx.coroutines.launch
9+
import kotlinx.coroutines.runBlocking
10+
11+
fun main(): Unit = runBlocking {
12+
val servient = Servient(
13+
servers = listOf(HttpProtocolServer(wait = true)),
14+
clientFactories = listOf(HttpProtocolClientFactory())
15+
)
16+
17+
// Register a shutdown hook
18+
Runtime.getRuntime().addShutdownHook(Thread {
19+
println("Application is shutting down. Performing cleanup...")
20+
launch { servient.shutdown() }
21+
})
22+
23+
val wot = Wot.create(servient)
24+
25+
val exposedThing = wot.produce {
26+
id = "myid"
27+
title = "MyThing"
28+
stringProperty("test"){
29+
title = "myProperty"
30+
minLength = 10
31+
}
32+
}
33+
34+
// Start `servient` in a separate coroutine
35+
val startJob = launch(Dispatchers.IO) {
36+
servient.start()
37+
}
38+
39+
// Add and expose the thing after `start()` has had time to begin
40+
servient.addThing(exposedThing)
41+
servient.expose("myid")
42+
43+
// Keep the coroutine active as long as `servient.start()` is running
44+
startJob.join()
45+
}
Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
package integration
22

3-
import kotlinx.coroutines.test.runTest
43
import ai.ancf.lmos.wot.Servient
54
import ai.ancf.lmos.wot.Wot
65
import ai.ancf.lmos.wot.binding.http.HttpProtocolClientFactory
76
import ai.ancf.lmos.wot.binding.http.HttpProtocolServer
7+
import ai.ancf.lmos.wot.thing.schema.*
8+
import kotlinx.coroutines.test.runTest
89
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
12+
private const val PROPERTY_NAME = "propertyName"
913

10-
class WoTIntegrationTest {
14+
private const val ACTION = "actionName"
15+
16+
class WoTIntegrationTest() {
1117

1218

1319
@Test
14-
fun `Should start http server`() = runTest {
20+
fun `Should fetch thing`() = runTest {
1521

1622
val servient = Servient(
1723
servers = listOf(HttpProtocolServer()),
@@ -22,15 +28,48 @@ class WoTIntegrationTest {
2228
val exposedThing = wot.produce {
2329
id = "myid"
2430
title = "MyThing"
31+
stringProperty(PROPERTY_NAME) {
32+
description = "Property description"
33+
minLength = 10
34+
readHandler = PropertyReadHandler { "propertyOutput" }
35+
writeHandler = PropertyWriteHandler { input -> input }
36+
}
37+
action<String, String>(ACTION){
38+
description = "Action description"
39+
input = stringSchema {
40+
description = "Input description"
41+
}
42+
output = stringSchema {
43+
description = "Output description"
44+
}
45+
actionHandler = ActionHandler { input, options -> "actionOutput: $input" }
46+
}
2547
}
2648

49+
exposedThing.setPropertyReadHandler(PROPERTY_NAME) { "propertyOutput" }
50+
exposedThing.setPropertyWriteHandler<String>(PROPERTY_NAME) { input -> input }
51+
exposedThing.setActionHandler<String, String>(ACTION) { input, options -> "actionOutput: $input" }
52+
2753
servient.start()
2854
servient.addThing(exposedThing)
2955
servient.expose("myid")
3056

31-
//val url = servient.fetchDirectory()
57+
//val fetchedThings = servient.fetchDirectory("http://localhost:8080")
58+
59+
//assertEquals(1, fetchedThings.size)
60+
61+
val thingDescription = wot.requestThingDescription("http://localhost:8080/myid")
62+
63+
val consumedThing = wot.consume(thingDescription)
64+
65+
assertEquals(consumedThing.id, exposedThing.id)
66+
67+
val readProperty = consumedThing.readProperty(PROPERTY_NAME)
68+
69+
assertEquals("propertyOutput", readProperty.value())
3270

33-
//val consumedThing = servient.fetch(url)
71+
val output = consumedThing.invokeAction(ACTION, "actionInput".toInteractionInputValue())
3472

73+
assertEquals("actionOutput + actionInput", output.value())
3574
}
3675
}

0 commit comments

Comments
 (0)