Skip to content

Commit dcdf630

Browse files
committed
feat(Willhaben): Search Node
1 parent e61eef1 commit dcdf630

File tree

10 files changed

+144
-18
lines changed

10 files changed

+144
-18
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version = "3.4.0-rc.1"
1+
version = "3.4.0-rc.2"

integration/willhaben/src/main/kotlin/me/snoty/integration/contrib/willhaben/api/WillhabenAPI.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import kotlinx.serialization.json.jsonArray
1111
import kotlinx.serialization.json.jsonObject
1212
import me.snoty.integration.contrib.utils.getOrThrow
1313
import me.snoty.integration.contrib.utils.parseNextPageProps
14+
import me.snoty.integration.contrib.willhaben.api.dto.WillhabenListing
15+
import me.snoty.integration.contrib.willhaben.api.dto.WillhabenSearchResult
16+
import me.snoty.integration.contrib.willhaben.api.dto.WillhabenWishlist
17+
import me.snoty.integration.contrib.willhaben.api.dto.cleanTitleFromWishlist
18+
import me.snoty.integration.contrib.willhaben.api.dto.parseListing
19+
import me.snoty.integration.contrib.willhaben.api.dto.parseSearchResult
1420
import me.snoty.integration.contrib.willhaben.utils.mapIf
1521
import org.koin.core.annotation.Single
1622
import org.koin.core.component.KoinComponent
@@ -23,6 +29,11 @@ interface WillhabenAPI {
2329
* @param cleanTitle whether to attempt to strip listing titles down to the actual names (without additional metadata)
2430
*/
2531
suspend fun fetchWishlist(creds: WillhabenCredentials, cleanTitle: Boolean): List<WillhabenListing>
32+
33+
/**
34+
* @param query the search query (stuff after `/iad/`)
35+
*/
36+
suspend fun search(query: String): List<WillhabenSearchResult>
2637
}
2738

2839
@Single
@@ -95,4 +106,23 @@ class WillhabenAPIImpl(private val httpClient: HttpClient) : WillhabenAPI, KoinC
95106

96107
return listings
97108
}
109+
110+
override suspend fun search(query: String): List<WillhabenSearchResult> {
111+
val url = URLBuilder().apply {
112+
protocol = URLProtocol.HTTPS
113+
host = WILLHABEN_HOST
114+
appendEncodedPathSegments("iad", query)
115+
}.build()
116+
117+
val response = httpClient.get(url)
118+
119+
val props = response.parseNextPageProps(json)
120+
val searchResults = props
121+
.getOrThrow("searchResult").jsonObject
122+
.getOrThrow("advertSummaryList").jsonObject
123+
.getOrThrow("advertSummary").jsonArray
124+
.map { it.jsonObject.parseSearchResult(json) }
125+
126+
return searchResults
127+
}
98128
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package me.snoty.integration.contrib.willhaben.api.dto
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.json.JsonObject
5+
import kotlinx.serialization.json.jsonArray
6+
import kotlinx.serialization.json.jsonObject
7+
import kotlinx.serialization.json.jsonPrimitive
8+
import me.snoty.integration.contrib.utils.getOrThrow
9+
10+
@Serializable
11+
data class WillhabenStatus(
12+
val id: String,
13+
val description: String,
14+
val statusId: Int,
15+
)
16+
17+
fun JsonObject.parseAttributes() = getOrThrow("attributes")
18+
.jsonObject
19+
.getOrThrow("attribute")
20+
.jsonArray
21+
.associate {
22+
val key = it.jsonObject.getOrThrow("name").jsonPrimitive.content
23+
val value = it.jsonObject.getOrThrow("values").jsonArray.map { item -> item.jsonPrimitive }
24+
key to value
25+
}
Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,9 @@
1-
package me.snoty.integration.contrib.willhaben.api
1+
package me.snoty.integration.contrib.willhaben.api.dto
22

33
import kotlinx.serialization.Serializable
44
import kotlinx.serialization.json.*
55
import me.snoty.integration.contrib.utils.getOrThrow
66

7-
@Serializable
8-
data class WillhabenStatus(
9-
val id: String,
10-
val description: String,
11-
val statusId: Int,
12-
)
13-
147
@Serializable
158
data class WillhabenListing(
169
val id: String,
@@ -23,11 +16,7 @@ data class WillhabenListing(
2316

2417
fun JsonObject.parseListing(json: Json): WillhabenListing {
2518
val id = this["id"]
26-
val attributes = getOrThrow("attributes").jsonObject.getOrThrow("attribute").jsonArray.associate {
27-
val key = it.jsonObject.getOrThrow("name").jsonPrimitive.content
28-
val value = it.jsonObject.getOrThrow("values").jsonArray.map { item -> item.jsonPrimitive }
29-
key to value
30-
}
19+
val attributes = parseAttributes()
3120

3221
val title = getOrThrow("description").jsonPrimitive.content
3322
val description = attributes["DESCRIPTION"]?.joinToString("\n") { it.content }
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package me.snoty.integration.contrib.willhaben.api.dto
2+
3+
import kotlinx.serialization.Serializable
4+
import kotlinx.serialization.json.*
5+
import me.snoty.integration.contrib.utils.getOrThrow
6+
7+
@Serializable
8+
data class WillhabenSearchResult(
9+
val id: String,
10+
val title: String,
11+
val description: String?,
12+
val price: Double,
13+
val status: WillhabenStatus,
14+
val attributes: Map<String, List<String>>,
15+
)
16+
17+
fun JsonObject.parseSearchResult(json: Json): WillhabenSearchResult {
18+
val id = this["id"]!!.jsonPrimitive.content
19+
val attributes = parseAttributes()
20+
21+
val title = getOrThrow("description").jsonPrimitive.content
22+
val description = attributes["BODY_DYN"]?.joinToString("\n") { it.content }
23+
val price = attributes["PRICE"]?.firstOrNull()?.doubleOrNull
24+
?: throw IllegalArgumentException("Price not found in listing")
25+
val status: WillhabenStatus = json.decodeFromJsonElement(getOrThrow("advertStatus"))
26+
27+
return WillhabenSearchResult(
28+
id = id,
29+
title = title,
30+
description = description,
31+
price = price,
32+
attributes = attributes.mapValues { (_, values) ->
33+
values.map { it.content }
34+
},
35+
status = status,
36+
)
37+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.integration.contrib.willhaben.api
1+
package me.snoty.integration.contrib.willhaben.api.dto
22

33
typealias WillhabenWishlist = List<WillhabenListing>
44

integration/willhaben/src/main/kotlin/me/snoty/integration/contrib/willhaben/listing/WillhabenListingNodeHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import me.snoty.integration.common.wiring.iterableStructOutput
1313
import me.snoty.integration.common.wiring.node.NodeHandler
1414
import me.snoty.integration.common.wiring.node.NodeSettings
1515
import me.snoty.integration.contrib.willhaben.api.WillhabenAPI
16-
import me.snoty.integration.contrib.willhaben.api.WillhabenListing
16+
import me.snoty.integration.contrib.willhaben.api.dto.WillhabenListing
1717
import org.koin.core.annotation.Single
1818

1919
data class ListingInput(
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package me.snoty.integration.contrib.willhaben.search
2+
3+
import kotlinx.serialization.Serializable
4+
import me.snoty.integration.common.annotation.RegisterNode
5+
import me.snoty.integration.common.model.NodePosition
6+
import me.snoty.integration.common.model.metadata.FieldDescription
7+
import me.snoty.integration.common.wiring.Node
8+
import me.snoty.integration.common.wiring.NodeHandleContext
9+
import me.snoty.integration.common.wiring.data.IntermediateData
10+
import me.snoty.integration.common.wiring.data.NodeOutput
11+
import me.snoty.integration.common.wiring.iterableStructOutput
12+
import me.snoty.integration.common.wiring.node.NodeHandler
13+
import me.snoty.integration.common.wiring.node.NodeSettings
14+
import me.snoty.integration.contrib.willhaben.api.WILLHABEN_HOST
15+
import me.snoty.integration.contrib.willhaben.api.WillhabenAPI
16+
import me.snoty.integration.contrib.willhaben.api.dto.WillhabenSearchResult
17+
import org.koin.core.annotation.Single
18+
19+
@Serializable
20+
data class WillhabenSearchSettings(
21+
override val name: String,
22+
@FieldDescription("Path after `https://${WILLHABEN_HOST}/iad/`")
23+
val query: String,
24+
) : NodeSettings
25+
26+
@RegisterNode(
27+
name = "willhaben_search",
28+
displayName = "Willhaben Suche",
29+
settingsType = WillhabenSearchSettings::class,
30+
outputType = WillhabenSearchResult::class,
31+
position = NodePosition.START,
32+
)
33+
@Single
34+
class WillhabenSearchNodeHandler(
35+
private val willhabenAPI: WillhabenAPI,
36+
) : NodeHandler {
37+
override suspend fun NodeHandleContext.process(
38+
node: Node,
39+
input: Collection<IntermediateData>
40+
): NodeOutput {
41+
val settings = node.settings as WillhabenSearchSettings
42+
val result = willhabenAPI.search(settings.query)
43+
return iterableStructOutput(result)
44+
}
45+
}

integration/willhaben/src/main/kotlin/me/snoty/integration/contrib/willhaben/wishlist/WillhabenWishlistNodeHandler.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import me.snoty.integration.common.wiring.node.NodeHandler
1414
import me.snoty.integration.common.wiring.node.NodeSettings
1515
import me.snoty.integration.contrib.willhaben.api.WillhabenAPI
1616
import me.snoty.integration.contrib.willhaben.api.WillhabenCredentials
17-
import me.snoty.integration.contrib.willhaben.api.WillhabenListing
17+
import me.snoty.integration.contrib.willhaben.api.dto.WillhabenListing
1818
import org.koin.core.annotation.Single
1919

2020
@Serializable
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package me.snoty.integration.contrib.willhaben.api
1+
package me.snoty.integration.contrib.willhaben.api.dto
22

33
import org.junit.jupiter.api.Test
44
import kotlin.test.assertEquals

0 commit comments

Comments
 (0)