Repository: softwaremill/sttp-openai (scala)

Issue:
Make roles in Message an ADT instead of String
              **suggestion:** This could be an ADT (in a separate PR) with a Custom option of course.

_Originally posted by @kciesielski in https://github.com/softwaremill/sttp-openai/pull/54#discussion_r1178803055_

Pull request description:

Patch:
diff --git a/README.md b/README.md
index 97301797..ecdb50e6 100644
--- a/README.md
+++ b/README.md
@@ -29,9 +29,9 @@ sttp openai is available for Scala 2.13 and Scala 3
 OpenAI API Offical Documentation https://platform.openai.com/docs/api-reference/completions
 
 ### Not yet implemented:
-  * Create chat completions SSE
-  * Create completions SSE
-  * List fine-tune events SSE
+    * Create chat completions SSE
+    * Create completions SSE
+    * List fine-tune events SSE
 
 ## Example
 
@@ -46,7 +46,7 @@ import sttp.openai.requests.completions.chat.Message
 
 // Create an instance of OpenAI providing your API secret-key
 
-val openAi: OpenAI = new OpenAI("your-secret-key")
+val openAI: OpenAI = new OpenAI("your-secret-key")
 
 // Create body of Chat Completions Request
 
diff --git a/core/src/main/scala/sttp/openai/requests/audio/transcriptions/TranscriptionConfig.scala b/core/src/main/scala/sttp/openai/requests/audio/transcriptions/TranscriptionConfig.scala
index 65f30600..48799080 100644
--- a/core/src/main/scala/sttp/openai/requests/audio/transcriptions/TranscriptionConfig.scala
+++ b/core/src/main/scala/sttp/openai/requests/audio/transcriptions/TranscriptionConfig.scala
@@ -162,7 +162,7 @@ object Language {
   case object Welsh extends Language("cy")
 
   /** @param customLanguage
-   *   use ISO-639-1 format to use desired language
-   */
+    *   use ISO-639-1 format to use desired language
+    */
   case class Custom(customLanguage: String) extends Language(customLanguage)
 }
diff --git a/core/src/main/scala/sttp/openai/requests/completions/chat/Message.scala b/core/src/main/scala/sttp/openai/requests/completions/chat/Message.scala
index b6d4f672..9613ce02 100644
--- a/core/src/main/scala/sttp/openai/requests/completions/chat/Message.scala
+++ b/core/src/main/scala/sttp/openai/requests/completions/chat/Message.scala
@@ -1,16 +1,43 @@
 package sttp.openai.requests.completions.chat
 
-import sttp.openai.json.SnakePickle
+import sttp.openai.json.{DeserializationException, SnakePickle}
+import ujson.Str
 
 /** @param role
-  *   The role of the author of this message. One of `system`, `user`, or `assistant`.
+  *   The role of the author of this message. One of [[Role]].
   * @param content
   *   The contents of the message.
   * @param name
   *   The name of the author of this message. May contain a-z, A-Z, 0-9, and underscores, with a maximum length of 64 characters.
   */
-case class Message(role: String, content: String, name: Option[String] = None)
+case class Message(role: Role, content: String, name: Option[String] = None)
 
 object Message {
   implicit val messageRW: SnakePickle.ReadWriter[Message] = SnakePickle.macroRW[Message]
 }
+
+sealed abstract class Role(val value: String)
+object Role {
+  case object System extends Role("system")
+
+  case object User extends Role("user")
+
+  case object Assistant extends Role("assistant")
+
+  case class Custom(customRole: String) extends Role(customRole)
+
+  val values: Set[Role] = Set(System, User, Assistant)
+
+  private val byRoleValue = values.map(role => role.value -> role).toMap
+
+  implicit val roleRW: SnakePickle.ReadWriter[Role] = SnakePickle
+    .readwriter[ujson.Value]
+    .bimap[Role](
+      role => SnakePickle.writeJs(role.value),
+      jsonValue =>
+        SnakePickle.read[ujson.Value](jsonValue) match {
+          case Str(value) => byRoleValue.getOrElse(value, Custom(value))
+          case e          => throw new DeserializationException(new Exception(s"Could not deserialize: $e"))
+        }
+    )
+}


Test patch:
diff --git a/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala b/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
index 389d57eb..df9e0e8e 100644
--- a/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
+++ b/core/src/test/scala/sttp/openai/fixtures/ChatFixture.scala
@@ -28,7 +28,7 @@ object ChatFixture {
       |   "choices": [
       |     {
       |       "message": {
-      |         "role": "assistance",
+      |         "role": "assistant",
       |         "content": "Hi there! How can I assist you today?",
       |         "name": "Fish"
       |       },
diff --git a/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala b/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala
index 8815ec0f..c677a7e2 100644
--- a/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala
+++ b/core/src/test/scala/sttp/openai/requests/completions/chat/ChatDataSpec.scala
@@ -24,7 +24,7 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     )
 
     val message: Message = Message(
-      role = "assistance",
+      role = Role.Assistant,
       content = "Hi there! How can I assist you today?",
       name = Some("Fish")
     )
@@ -55,7 +55,7 @@ class ChatDataSpec extends AnyFlatSpec with Matchers with EitherValues {
     // given
     val messages: Seq[Message] = Seq(
       Message(
-        role = "user",
+        role = Role.User,
         content = "Hello!",
         name = Some("Andrzej")
       )