<commit_before>package info.mukel.telegrambot4s.marshalling

import akka.http.scaladsl.marshalling.{Marshaller, Marshalling, ToEntityMarshaller}
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, MediaTypes, Multipart}
import akka.http.scaladsl.unmarshalling.{Unmarshaller, _}
import com.typesafe.scalalogging.StrictLogging
import info.mukel.telegrambot4s.methods._
import info.mukel.telegrambot4s.models.{ChatId, _}
import org.json4s.JsonAST.{JInt, JString}
import org.json4s.ext.EnumNameSerializer
import org.json4s.jackson.JsonMethods._
import org.json4s.{CustomSerializer, DefaultFormats, Extraction, Formats, NoTypeHints}

/**
  * De/serialization support for JSON and multipart API requests.
  */
object HttpMarshalling extends StrictLogging {

  object ChatIdSerializer extends CustomSerializer[ChatId]( format => (
    {
      case JString(channel)  => ChatId(channel)
      case JInt(chat) => ChatId(chat.toLong)
    },
    {
      case ChatId.Channel(channel)  => JString(channel)
      case ChatId.Chat(chat)  => JInt(chat)
    }
  ))

  implicit val formats: Formats = (
    new Formats {
      val dateFormat = DefaultFormats.lossless.dateFormat
      override val typeHints = NoTypeHints
      // Throws MappingException if Option[_] cannot be parsed.
      override def strictOptionParsing: Boolean = true
    } +
    new EnumNameSerializer(ChatAction) +
    new EnumNameSerializer(ParseMode) +
    new EnumNameSerializer(ChatType) +
    new EnumNameSerializer(Currency) +
    new EnumNameSerializer(CountryCode) +
    new EnumNameSerializer(UpdateType) +
    new EnumNameSerializer(MessageEntityType) +
    new EnumNameSerializer(MaskPositionType) +
    ChatIdSerializer
  )

  def toJson[T](t: T): String = compact(render(Extraction.decompose(t).underscoreKeys))

  def fromJson[T: Manifest](json: String): T = parse(json).camelizeKeys.extract[T]

  implicit def camelCaseJsonUnmarshaller[T : Manifest]: FromEntityUnmarshaller[T] = {
    Unmarshaller
      .stringUnmarshaller
      .forContentTypes(ContentTypes.`application/json`)
      .map(fromJson[T])
  }

  private def camelToUnderscores(name: String): String = "[A-Z\\d]".r.replaceAllIn(name, { m =>
    "_" + m.group(0).toLowerCase()
  })

  implicit def underscore_case_marshaller[T]: ToEntityMarshaller[ApiRequest[T]] = {

    Marshaller.strict {
      // JSON-only request
      case request: ApiRequestJson[T] =>
        Marshalling.Opaque(() => HttpEntity(ContentTypes.`application/json`, toJson[ApiRequestJson[_]](request)))

      // Request with file payload
      case request: ApiRequestMultipart[T] => {

        def unwrap(f: Any): Any = f match {
          case Some(x)  => unwrap(x)
          case Left(x)  => unwrap(x)
          case Right(x) => unwrap(x)
          case _        => f
        }

        val fields = request.getClass.getDeclaredFields
        val values = request.productIterator
        val fieldNames = fields map (_.getName)

        val pairs = fieldNames
          .zip(values.toSeq)
          .map {
            case (k, v) => (camelToUnderscores(k), unwrap(v))
          }

        val params = pairs.filterNot(_._2 == None)

        val parts = params map {
          case (k, v) => v match {
            case primitive @ (_: Int | _: Long | _: Float | _: Double | _: Boolean | _: String) =>
              Multipart.FormData.BodyPart(k, HttpEntity(primitive.toString))

            // Handle files
            case InputFile.FileId(fileId) =>
              Multipart.FormData.BodyPart(k, HttpEntity(fileId))

            case InputFile.Path(path) =>
                Multipart.FormData.BodyPart.fromPath(k, MediaTypes.`application/octet-stream`, path)
<commit_msg>Simplfy JSON serialization for multi-part requests<commit_after>package info.mukel.telegrambot4s.marshalling

import akka.http.scaladsl.marshalling.{Marshaller, Marshalling, ToEntityMarshaller}
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, MediaTypes, Multipart}
import akka.http.scaladsl.unmarshalling.{Unmarshaller, _}
import com.typesafe.scalalogging.StrictLogging
import info.mukel.telegrambot4s.methods._
import info.mukel.telegrambot4s.models.{ChatId, _}
import org.json4s.JsonAST.{JInt, JString}
import org.json4s.ext.EnumNameSerializer
import org.json4s.jackson.JsonMethods._
import org.json4s.{CustomSerializer, DefaultFormats, Extraction, Formats, NoTypeHints}

/**
  * De/serialization support for JSON and multipart API requests.
  */
object HttpMarshalling extends StrictLogging {

  object ChatIdSerializer extends CustomSerializer[ChatId]( format => (
    {
      case JString(channel)  => ChatId(channel)
      case JInt(chat) => ChatId(chat.toLong)
    },
    {
      case ChatId.Channel(channel)  => JString(channel)
      case ChatId.Chat(chat)  => JInt(chat)
    }
  ))

  implicit val formats: Formats = (
    new Formats {
      val dateFormat = DefaultFormats.lossless.dateFormat
      override val typeHints = NoTypeHints
      // Throws MappingException if Option[_] cannot be parsed.
      override def strictOptionParsing: Boolean = true
    } +
    new EnumNameSerializer(ChatAction) +
    new EnumNameSerializer(ParseMode) +
    new EnumNameSerializer(ChatType) +
    new EnumNameSerializer(Currency) +
    new EnumNameSerializer(CountryCode) +
    new EnumNameSerializer(UpdateType) +
    new EnumNameSerializer(MessageEntityType) +
    new EnumNameSerializer(MaskPositionType) +
    ChatIdSerializer
  )

  def toJson[T](t: T): String = compact(render(Extraction.decompose(t).underscoreKeys))

  def fromJson[T: Manifest](json: String): T = parse(json).camelizeKeys.extract[T]

  implicit def camelCaseJsonUnmarshaller[T : Manifest]: FromEntityUnmarshaller[T] = {
    Unmarshaller
      .stringUnmarshaller
      .forContentTypes(ContentTypes.`application/json`)
      .map(fromJson[T])
  }

  private def camelToUnderscores(name: String): String = "[A-Z\\d]".r.replaceAllIn(name, { m =>
    "_" + m.group(0).toLowerCase()
  })

  implicit def underscore_case_marshaller[T]: ToEntityMarshaller[ApiRequest[T]] = {

    Marshaller.strict {
      // JSON-only request
      case request: ApiRequestJson[T] =>
        Marshalling.Opaque(() => HttpEntity(ContentTypes.`application/json`, toJson[ApiRequestJson[T]](request)))

      // Request with file payload
      case request: ApiRequestMultipart[T] => {

        val fields = request.getClass.getDeclaredFields
        val values = request.productIterator
        val fieldNames = fields map (_.getName)

        val params = fieldNames
          .zip(values.toSeq)
          .collect {
            // Ignored absent parameters
            case (k, Some(v)) => (camelToUnderscores(k), v)
          }

        val parts = params map {
          case (k, v) => v match {
            case primitive @ (_: Int | _: Long | _: Float | _: Double | _: Boolean | _: String) =>
              Multipart.FormData.BodyPart(k, HttpEntity(primitive.toString))

            // Handle files
            case InputFile.FileId(fileId) =>
              Multipart.FormData.BodyPart(k, HttpEntity(fileId))

            case InputFile.Path(path) =>
                Multipart.FormData.BodyPart.fromPath(k, MediaTypes.`application/octet-stream`, path)

{'fraction_non_alphanumeric': 0.10946170921198668, 'fraction_numerical': 0.0023584905660377358, 'mean_word_length': 3.253097345132743, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5327431', 'n_tokens_mistral': 2229, 'n_tokens_neox': 2226, 'n_words': 435}