Skip to content

Commit

Permalink
Merge pull request #19 from theBird2Bones/posting-data
Browse files Browse the repository at this point in the history
Add posting data model
  • Loading branch information
little-inferno authored Aug 21, 2024
2 parents 068713b + deb63bd commit 4d10390
Show file tree
Hide file tree
Showing 12 changed files with 135 additions and 38 deletions.
2 changes: 2 additions & 0 deletions modules/core/src/main/scala/muffin/api/Websocket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ object Websocket {

def connect(): F[Unit] = httpClient.websocketWithListeners(uri, headers, backoffSettings, listeners)

/** Can repeat listener in case server sends data to client. MM sends event of bot actions
*/
def addListener[EventData: From](
eventType: EventType,
onEventListener: EventData => F[Unit]
Expand Down
29 changes: 23 additions & 6 deletions modules/core/src/main/scala/muffin/codec/CodecSupport.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ trait JsonRequestBuilder[T, To[_]]() { self =>
trait JsonResponseBuilder[From[_], Params <: Tuple] {
def field[X: From](name: String): JsonResponseBuilder[From, X *: Params]

def fieldMap[X: From, B](name: String)(f: X => B): JsonResponseBuilder[From, B *: Params]
def fieldMap[X: From, B](name: String)(f: X => Either[MuffinError.Decoding, B])
: JsonResponseBuilder[From, B *: Params]

def rawField(name: String): JsonResponseBuilder[From, Option[String] *: Params]

Expand Down Expand Up @@ -592,7 +593,7 @@ trait CodecSupportLow[To[_], From[_]] extends PrimitivesSupport[To, From] {

given AttachmentFrom: From[Attachment] =
parsing
.fieldMap[Option[List[Action]], List[Action]]("actions")(_.getOrElse(Nil))
.fieldMap[Option[List[Action]], List[Action]]("actions")(_.getOrElse(Nil).asRight)
.field[Option[String]]("footer_icon")
.field[Option[String]]("footer")
.field[Option[String]]("thumb_url")
Expand Down Expand Up @@ -624,12 +625,28 @@ trait CodecSupportLow[To[_], From[_]] extends PrimitivesSupport[To, From] {

given PostFrom: From[Post] =
parsing
.field[Option[Props]]("props")
.fieldMap[Option[Props], Props]("props")(_.getOrElse(Props.empty).asRight)
.field[Option[List[FileId]]]("file_ids")
.field[ChannelId]("channel_id")
.field[UserId]("user_id")
.field[String]("message")
.field[MessageId]("id")
.build {
case id *: message *: props *: EmptyTuple => Post(id, message, props.getOrElse(Props.empty))
}
.build(Post.apply.tupled)

given PostedEventDataFrom: From[PostedEventData] =
parsing
.fieldMap[String, Post]("post")(Decode[Post].apply)
.field[String]("sender_name")
.field[ChannelType]("channel_type")
.field[String]("channel_name")
.build(PostedEventData.apply.tupled)

given ChannelTypeFrom: From[ChannelType] =
parsing[String, ChannelType] {
case "O" => ChannelType.Channel
case "D" => ChannelType.Direct
case unknown => ChannelType.Unknown(unknown)
}

given EventFrom[A: From]: From[Event[A]] =
parsing
Expand Down
8 changes: 4 additions & 4 deletions modules/core/src/main/scala/muffin/model/Posts.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import muffin.codec.*

case class Post(
id: MessageId,
message: String,
// create_at: Long,
// update_at: Long,
// delete_at: Long,
// edit_at: Long,
// user_id: UserId,
// channel_id: ChannelId,
userId: UserId,
channelId: ChannelId,
fileIds: Option[List[FileId]],
// root_id: MessageId,
// original_id: MessageId,
message: String,
// `type`: String,
props: Props = Props.empty
// hashtag: Option[String],
// file_ids: List[String],
// pending_post_id: Option[String],
)

Expand Down
15 changes: 8 additions & 7 deletions modules/core/src/main/scala/muffin/model/websocket/domain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,12 @@ object domain {

}

}
case class PostedEventData(channelName: String, channelType: ChannelType, senderName: String, post: Post)

enum ChannelType {
case Direct
case Channel
case Unknown(repr: String)
}

case class PostedEventData(
channelName: String,
teamId: String,
senderName: String,
post: Post
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
{
"response": {
"id": "34b52dfdd69f485bb0e70d1879",
"user_id": "34b52dfdd69f485bb0e70d1879",
"channel_id": "34b52dfdd69f485bb0e70d1879",
"message": "text",
"props": {
"attachments": [
Expand Down
2 changes: 2 additions & 0 deletions modules/core/src/test/resources/http/test/posts/Post.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
},
"response": {
"id": "34b52dfdd69f485bb0e70d1879",
"user_id": "34b52dfdd69f485bb0e70d1879",
"channel_id": "34b52dfdd69f485bb0e70d1879",
"message": "message",
"props": {
"attachments": [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"channel_name": "awif8o9dxidst8suxf47gwc3gw__hpgdxehuti8huqei4yoyfcdcoc",
"channel_type": "D",
"sender_name": "@danil",
"post": "{\n \"id\": \"6pzso8hr77gnirkntn1qe9hxso\",\n \"create_at\": 1723751876786,\n \"update_at\": 1723751876786,\n \"edit_at\": 0,\n \"delete_at\": 0,\n \"is_pinned\": false,\n \"user_id\": \"hpgdxehuti8huqei4yoyfcdcoc\",\n \"channel_id\": \"rawjhctgq3ytpmgyfcyn3zowjc\",\n \"root_id\": \"\",\n \"original_id\": \"\",\n \"message\": \"\",\n \"type\": \"\",\n \"props\": {\n \"disable_group_highlight\": true\n },\n \"hashtags\": \"\",\n \"file_ids\": [\n \"a7frxuzgrp8ftkifpaxdemaphr\"\n ],\n \"pending_post_id\": \"hpgdxehuti8huqei4yoyfcdcoc:1723751876740\",\n \"reply_count\": 0,\n \"last_reply_at\": 0,\n \"participants\": null,\n \"metadata\": {\n \"files\": [\n {\n \"id\": \"a7frxuzgrp8ftkifpaxdemaphr\",\n \"user_id\": \"hpgdxehuti8huqei4yoyfcdcoc\",\n \"post_id\": \"6pzso8hr77gnirkntn1qe9hxso\",\n \"channel_id\": \"rawjhctgq3ytpmgyfcyn3zowjc\",\n \"create_at\": 1723751875886,\n \"update_at\": 1723751875886,\n \"delete_at\": 0,\n \"name\": \"docs.yaml\",\n \"extension\": \"yaml\",\n \"size\": 7850,\n \"mime_type\": \"\",\n \"mini_preview\": null,\n \"remote_id\": \"\",\n \"archived\": false\n }\n ]\n }\n }",
"channel_display_name": "@danil",
"mentions": "[\"awif8o9dxidst8suxf47gwc3gw\"]",
"otherFile": "true",
"set_online": true,
"team_id": ""
}
28 changes: 26 additions & 2 deletions modules/core/src/test/scala/muffin/api/ApiTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ trait ApiTest[To[_], From[_]](integration: String, codecSupport: CodecSupport[To
.build[domain.TestObject] {
case field *: EmptyTuple => domain.TestObject.apply(field)
}

//
Scenario(s"process websocket event $integration") {
for {
listenedEvent <- Deferred[IO, domain.TestObject]
Expand All @@ -301,7 +301,7 @@ trait ApiTest[To[_], From[_]](integration: String, codecSupport: CodecSupport[To
_ <- websocketFiber.join
} yield assert(event == domain.TestObject.default)
}

//
Scenario(s"Different connections work independent $integration") {
val badEvent = domain.TestObject("broken")
for {
Expand Down Expand Up @@ -339,6 +339,30 @@ trait ApiTest[To[_], From[_]](integration: String, codecSupport: CodecSupport[To
events.contains(badEvent)
)
}

Scenario(s"process posting event with files $integration") {
for {
listenedEvent <- Deferred[IO, PostedEventData]
websocketFiber <- apiClient
.websocket()
.flatMap(
_.addListener[PostedEventData](
EventType.Posted,
event => listenedEvent.complete(event).void
)
.connect()
.start
)
expected <- loadResource(
"websockets/posting/postingWithFileIds.json"
)
.flatMap(raw =>
IO.fromEither(Decode[PostedEventData].apply(raw))
)
event <- listenedEvent.get.timeout(2.seconds)
_ <- websocketFiber.join
} yield assert(event == expected)
}
}

Feature("file api") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package muffin.interop.json.circe
import java.time.*

import cats.arrow.FunctionK
import cats.syntax.all.given
import cats.syntax.all.*

import io.circe.*
import io.circe.Decoder.Result
Expand Down Expand Up @@ -140,8 +140,30 @@ trait CodecLow2 extends CodecSupport[Encoder, Decoder] {
def field[X: Decoder](name: String): JsonResponseBuilder[Decoder, X *: Decoders] =
CirceResponseBuilder[X *: Decoders](decoders.flatMap(all => Decoder[X].at(name).map(_ *: all)))

def fieldMap[X: Decoder, B](name: String)(f: X => B): JsonResponseBuilder[Decoder, B *: Decoders] =
CirceResponseBuilder[B *: Decoders](decoders.flatMap(all => Decoder[X].at(name).map(x => f(x) *: all)))
def fieldMap[X: Decoder, B](name: String)(
f: X => Either[MuffinError.Decoding, B]
): JsonResponseBuilder[Decoder, B *: Decoders] =
CirceResponseBuilder[B *: Decoders](
for {
all <- decoders
x <- Decoder[X].at(name).map(f)
b <-
new Decoder[B] {
override def apply(c: HCursor): Result[B] = tryDecode(c)

override def tryDecode(c: ACursor): Result[B] =
x match {
case Left(err) =>
DecodingFailure(
DecodingFailure.Reason.CustomReason(s"Unable to parse field: $name due to ${err.message}"),
c
).asLeft
case Right(value) => value.asRight
}

}
} yield b *: all
)

def rawField(name: String): JsonResponseBuilder[Decoder, Option[String] *: Decoders] =
CirceResponseBuilder[Option[String] *: Decoders] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ import org.scalatest.featurespec.AsyncFeatureSpec

import muffin.api.*
import muffin.http.{Body, EventListener, HttpClient, Method, Params}
import muffin.model.websocket.domain.{Event, EventType}
import muffin.model.websocket.domain.RawJson
import muffin.model.websocket.domain.{Event, EventType, RawJson}

class CirceApiTest extends ApiTest[Encoder, Decoder]("circe", codec) {

Expand Down Expand Up @@ -54,15 +53,21 @@ class CirceApiTest extends ApiTest[Encoder, Decoder]("circe", codec) {
headers: Map[String, String],
backoffSettings: BackoffSettings,
listeners: List[EventListener[IO]]
): IO[Unit] = events.traverse_(event => listeners.traverse_(_.onEvent(event)))
): IO[Unit] = events.flatMap(_.traverse_(event => listeners.traverse_(_.onEvent(event))))

}

private val events = List(
Event(
EventType.Hello,
RawJson.from(Encoder[domain.TestObject].apply(domain.TestObject.default).toString)
)
private val events = loadResource(
"websockets/posting/postingWithFileIds.json"
)
.map(postingEvent =>
List(
Event(
EventType.Hello,
RawJson.from(Encoder[domain.TestObject].apply(domain.TestObject.default).toString)
),
Event(EventType.Posted, RawJson.from(postingEvent))
)
)

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package muffin.interop.json.zio
import java.time.{Instant, LocalDateTime, ZoneId}
import scala.reflect.ClassTag

import cats.syntax.all.*

import zio.json.*
import zio.json.JsonDecoder.{JsonError, UnsafeJson}
import zio.json.ast.Json
Expand Down Expand Up @@ -132,11 +134,18 @@ trait CodecLow extends CodecSupport[JsonEncoder, JsonDecoder] {
summon[JsonDecoder[X]].asInstanceOf[JsonDecoder[Any]] :: decoders
)

def fieldMap[X: JsonDecoder, B](name: String)(f: X => B): JsonResponseBuilder[JsonDecoder, B *: Params] =
def fieldMap[X: JsonDecoder, B](name: String)(
f: X => Either[MuffinError.Decoding, B]
): JsonResponseBuilder[JsonDecoder, B *: Params] = {
summon[JsonDecoder[Option[X]]]
summon[JsonDecoder[String]]
new ZioResponseBuilder[B *: Params](
name :: stateNames,
summon[JsonDecoder[X]].map(f).asInstanceOf[JsonDecoder[Any]] :: decoders
summon[JsonDecoder[X]]
.mapOrFail(x => f(x).leftMap(_.message))
.asInstanceOf[JsonDecoder[Any]] :: decoders
)
}

override def internal[X: JsonDecoder](name: String): JsonResponseBuilder[JsonDecoder, X *: Params] =
new ZioResponseBuilder[X *: Params](
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,17 @@ class ZioApiTest extends ApiTest[JsonEncoder, JsonDecoder]("zio", codec) {
headers: Map[String, String],
backoffSettings: BackoffSettings,
listeners: List[EventListener[IO]]
): IO[Unit] = events.traverse_(event => listeners.traverse_(_.onEvent(event)))
): IO[Unit] = events.flatMap(_.traverse_(event => listeners.traverse_(_.onEvent(event))))

private val events = List(
Event(
EventType.Hello,
RawJson.from(domain.TestObject.default.toJson)
private val events = loadResource("websockets/posting/postingWithFileIds.json").map(postingEvent =>
List(
Event(
EventType.Hello,
RawJson.from(domain.TestObject.default.toJson)
),
Event(EventType.Posted, RawJson.from(postingEvent))
)
)

}
}

Expand Down

0 comments on commit 4d10390

Please sign in to comment.