Skip to content

Commit 7ad5576

Browse files
committed
test(anthropic-client): add tests for message serialization
Enhanced testing for Message and Content serialization in the anthropic-client module, including support for multiple content types like text and image. Added necessary dependencies to build configurations.
1 parent b220f50 commit 7ad5576

File tree

9 files changed

+488
-8
lines changed

9 files changed

+488
-8
lines changed

anthropic-client/anthropic-client-core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ kotlin {
3838
implementation(libs.app.cash.turbine)
3939
implementation("com.tngtech.archunit:archunit-junit5:1.1.0")
4040
implementation("org.reflections:reflections:0.10.2")
41+
implementation(libs.org.skyscreamer.jsonassert)
4142
}
4243
}
4344
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.tddworks.anthropic.api.messages.api
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.builtins.ListSerializer
5+
import kotlinx.serialization.descriptors.SerialDescriptor
6+
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
7+
import kotlinx.serialization.encoding.Decoder
8+
import kotlinx.serialization.encoding.Encoder
9+
import kotlinx.serialization.json.*
10+
11+
object ContentSerializer : KSerializer<Content> {
12+
override val descriptor: SerialDescriptor
13+
get() = buildClassSerialDescriptor("Content")
14+
15+
override fun deserialize(decoder: Decoder): Content {
16+
val jsonElement = decoder.decodeSerializableValue(JsonElement.serializer())
17+
return when (jsonElement) {
18+
is JsonPrimitive -> Content.TextContent(jsonElement.content)
19+
is JsonArray -> {
20+
val items = jsonElement.map { element ->
21+
val jsonObj = element.jsonObject
22+
23+
when (jsonObj["type"]?.jsonPrimitive?.content) {
24+
"text" -> BlockMessageContent.TextContent(
25+
text = jsonObj["text"]?.jsonPrimitive?.content
26+
?: throw IllegalArgumentException("Missing text")
27+
)
28+
29+
"image" -> BlockMessageContent.ImageContent(
30+
source = BlockMessageContent.ImageContent.Source(
31+
mediaType = jsonObj["source"]?.jsonObject?.get("media_type")?.jsonPrimitive?.content
32+
?: throw IllegalArgumentException("Missing media_type"),
33+
data = jsonObj["source"]?.jsonObject?.get("data")?.jsonPrimitive?.content
34+
?: throw IllegalArgumentException("Missing data"),
35+
type = jsonObj["source"]?.jsonObject?.get("type")?.jsonPrimitive?.content
36+
?: throw IllegalArgumentException("Missing type")
37+
)
38+
)
39+
40+
else -> throw IllegalArgumentException("Unsupported content block type")
41+
}
42+
43+
}
44+
Content.BlockContent(blocks = items)
45+
}
46+
47+
else -> throw IllegalArgumentException("Unsupported content format")
48+
}
49+
}
50+
51+
override fun serialize(encoder: Encoder, value: Content) {
52+
when (value) {
53+
is Content.TextContent -> encoder.encodeString(value.text)
54+
is Content.BlockContent -> encoder.encodeSerializableValue(
55+
ListSerializer(BlockMessageContent.serializer()), value.blocks
56+
)
57+
}
58+
}
59+
}

anthropic-client/anthropic-client-core/src/commonMain/kotlin/com/tddworks/anthropic/api/messages/api/Message.kt

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.tddworks.anthropic.api.messages.api
22

3+
import kotlinx.serialization.SerialName
34
import kotlinx.serialization.Serializable
45

56
/**
@@ -13,13 +14,73 @@ import kotlinx.serialization.Serializable
1314
* Example with a single user message:
1415
*
1516
* [{"role": "user", "content": "Hello, Claude"}]
17+
*
18+
* Each input message content may be either a single string or an array of content blocks, where each block has a specific type. Using a string for content is shorthand for an array of one content block of type "text". The following input messages are equivalent:
19+
* {"role": "user", "content": "Hello, Claude"}
20+
* {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]}
1621
*/
1722
@Serializable
1823
data class Message(
1924
val role: Role,
20-
val content: String,
25+
val content: Content,
2126
) {
2227
companion object {
23-
fun user(content: String) = Message(Role.User, content)
28+
fun user(content: String) = Message(Role.User, Content.TextContent(content))
29+
}
30+
}
31+
32+
@Serializable(with = ContentSerializer::class)
33+
sealed interface Content {
34+
data class TextContent(
35+
val text: String,
36+
) : Content
37+
38+
data class BlockContent(
39+
val blocks: List<BlockMessageContent>,
40+
) : Content
41+
}
42+
43+
44+
/**
45+
* https://docs.anthropic.com/en/docs/build-with-claude/vision#prompt-examples
46+
* {
47+
* "role": "user",
48+
* "content": [
49+
* {
50+
* "type": "image",
51+
* "source": {
52+
* "type": "base64",
53+
* "media_type": image1_media_type,
54+
* "data": image1_data,
55+
* },
56+
* },
57+
* {
58+
* "type": "text",
59+
* "text": "Describe this image."
60+
* }
61+
* ],
62+
* }
63+
*/
64+
@Serializable
65+
sealed interface BlockMessageContent {
66+
67+
@Serializable
68+
@SerialName("image")
69+
data class ImageContent(
70+
val source: Source
71+
) : BlockMessageContent {
72+
@Serializable
73+
data class Source(
74+
@SerialName("media_type") val mediaType: String,
75+
val data: String,
76+
val type: String
77+
)
2478
}
25-
}
79+
80+
@Serializable
81+
@SerialName("text")
82+
data class TextContent(
83+
val text: String,
84+
) : BlockMessageContent
85+
86+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.tddworks.anthropic.api.messages.api
2+
3+
import kotlinx.serialization.KSerializer
4+
import kotlinx.serialization.SerializationException
5+
import kotlinx.serialization.json.JsonContentPolymorphicSerializer
6+
import kotlinx.serialization.json.JsonElement
7+
import kotlinx.serialization.json.jsonObject
8+
9+
/**
10+
* {
11+
* "role": "user",
12+
* "content": [
13+
* {
14+
* "type": "image",
15+
* "source": {
16+
* "type": "base64",
17+
* "media_type": image1_media_type,
18+
* "data": image1_data,
19+
* },
20+
* },
21+
* {
22+
* "type": "text",
23+
* "text": "Describe this image."
24+
* }
25+
* ],
26+
* }
27+
*/
28+
//internal object MessageContentSerializer :
29+
// JsonContentPolymorphicSerializer<Content>(Content::class) {
30+
// override fun selectDeserializer(element: JsonElement): KSerializer<out Content> {
31+
// val jsonObject = element.jsonObject
32+
// return when {
33+
// "source" in jsonObject -> BlockMessageContent.ImageContent.serializer()
34+
// "text" in jsonObject -> BlockMessageContent.TextContent.serializer()
35+
// else -> throw SerializationException("Unknown Content type")
36+
// }
37+
//
38+
// }
39+
//}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package com.tddworks.anthropic.api.messages.api
2+
3+
import kotlinx.serialization.json.Json
4+
import org.junit.jupiter.api.Test
5+
import org.skyscreamer.jsonassert.JSONAssert
6+
7+
class BlockMessageContentTest {
8+
9+
@Test
10+
fun `should serialize image message content`() {
11+
// Given
12+
val messageContent = BlockMessageContent.ImageContent(
13+
source = BlockMessageContent.ImageContent.Source(
14+
mediaType = "image1_media_type",
15+
data = "image1_data",
16+
type = "base64",
17+
),
18+
)
19+
20+
// When
21+
val result = Json.encodeToString(
22+
BlockMessageContent.serializer(),
23+
messageContent
24+
)
25+
26+
// Then
27+
JSONAssert.assertEquals(
28+
"""
29+
{
30+
"type": "image",
31+
"source": {
32+
"type": "base64",
33+
"media_type": "image1_media_type",
34+
"data": "image1_data"
35+
}
36+
}
37+
""".trimIndent(),
38+
result,
39+
false
40+
)
41+
}
42+
43+
@Test
44+
fun `should serialize message content`() {
45+
// Given
46+
val messageContent = BlockMessageContent.TextContent(
47+
text = "some-text",
48+
)
49+
50+
// When
51+
val result = Json.encodeToString(
52+
BlockMessageContent.serializer(),
53+
messageContent
54+
)
55+
56+
// Then
57+
JSONAssert.assertEquals(
58+
"""
59+
{
60+
"text": "some-text",
61+
"type": "text"
62+
}
63+
""".trimIndent(),
64+
result,
65+
false
66+
)
67+
}
68+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.tddworks.anthropic.api.messages.api
2+
3+
import kotlinx.serialization.json.Json
4+
import org.junit.jupiter.api.Assertions.assertEquals
5+
import org.junit.jupiter.api.Test
6+
import org.skyscreamer.jsonassert.JSONAssert
7+
8+
9+
/**
10+
* Each input message content may be either a single string or an array of content blocks, where each block has a specific type. Using a string for content is shorthand for an array of one content block of type "text". The following input messages are equivalent:
11+
* {"role": "user", "content": "Hello, Claude"}
12+
* {"role": "user", "content": [{"type": "text", "text": "Hello, Claude"}]}
13+
*/
14+
class ContentTest {
15+
16+
@Test
17+
fun `should serialize multiple content`() {
18+
// Given
19+
val content = Content.BlockContent(
20+
listOf(
21+
BlockMessageContent.ImageContent(
22+
source = BlockMessageContent.ImageContent.Source(
23+
mediaType = "image1_media_type",
24+
data = "image1_data",
25+
type = "base64",
26+
),
27+
),
28+
BlockMessageContent.TextContent(
29+
text = "some-text",
30+
),
31+
)
32+
)
33+
34+
// When
35+
val result = Json.encodeToString(
36+
Content.serializer(),
37+
content
38+
)
39+
40+
// Then
41+
JSONAssert.assertEquals(
42+
"""
43+
[
44+
{
45+
"source": {
46+
"data": "image1_data",
47+
"media_type": "image1_media_type",
48+
"type": "base64"
49+
},
50+
"type": "image"
51+
},
52+
{
53+
"text": "some-text",
54+
"type": "text"
55+
}
56+
]
57+
""".trimIndent(),
58+
result,
59+
false
60+
)
61+
}
62+
63+
@Test
64+
fun `should serialize single string content`() {
65+
// Given
66+
val content = Content.TextContent("Hello, Claude")
67+
68+
// When
69+
val result = Json.encodeToString(
70+
Content.serializer(),
71+
content
72+
)
73+
74+
// Then
75+
assertEquals(
76+
"""
77+
"Hello, Claude"
78+
""".trimIndent(), result
79+
)
80+
}
81+
82+
}

0 commit comments

Comments
 (0)