diff --git a/build.sbt b/build.sbt index b3a14aa..8a117cc 100644 --- a/build.sbt +++ b/build.sbt @@ -2,8 +2,8 @@ name in ThisBuild := "zio-slack" organization in ThisBuild := "com.github.dapperware" -val mainScala = "2.12.10" -val allScala = Seq("2.13.1", mainScala) +val mainScala = "2.12.12" +val allScala = Seq("2.13.4", mainScala) inThisBuild( List( diff --git a/client/src/main/scala/com/github/dapperware/slack/api/SlackAuth.scala b/client/src/main/scala/com/github/dapperware/slack/api/SlackAuth.scala index 95b6cfd..4bc46b0 100644 --- a/client/src/main/scala/com/github/dapperware/slack/api/SlackAuth.scala +++ b/client/src/main/scala/com/github/dapperware/slack/api/SlackAuth.scala @@ -1,7 +1,5 @@ package com.github.dapperware.slack.api -import com.github.dapperware.slack.models.AuthIdentity -import com.github.dapperware.slack.{ SlackEnv, SlackError, SlackExtractors } import com.github.dapperware.slack.models.AuthIdentity import com.github.dapperware.slack.{ SlackEnv, SlackError, SlackExtractors } import zio.ZIO diff --git a/client/src/main/scala/com/github/dapperware/slack/api/SlackChats.scala b/client/src/main/scala/com/github/dapperware/slack/api/SlackChats.scala index 95569ff..cfcb843 100644 --- a/client/src/main/scala/com/github/dapperware/slack/api/SlackChats.scala +++ b/client/src/main/scala/com/github/dapperware/slack/api/SlackChats.scala @@ -2,8 +2,6 @@ package com.github.dapperware.slack.api import com.github.dapperware.slack.models.{ Attachment, Block, UpdateResponse } import com.github.dapperware.slack.{ SlackEnv, SlackError } -import com.github.dapperware.slack.{ SlackEnv, SlackError } -import com.github.dapperware.slack.models.{ Attachment, Block, UpdateResponse } import io.circe.Json import io.circe.syntax._ import zio.ZIO diff --git a/client/src/main/scala/com/github/dapperware/slack/models/Block.scala b/client/src/main/scala/com/github/dapperware/slack/models/Block.scala index fe664e3..36b4638 100644 --- a/client/src/main/scala/com/github/dapperware/slack/models/Block.scala +++ b/client/src/main/scala/com/github/dapperware/slack/models/Block.scala @@ -161,7 +161,7 @@ case class ButtonElement(text: PlainTextObject, case class StaticSelectElement(placeholder: PlainTextObject, action_id: String, options: Seq[OptionObject], - option_groups: Seq[OptionGroupObject], + option_groups: Option[Seq[OptionGroupObject]] = None, initial_option: Option[Either[OptionObject, OptionGroupObject]] = None, confirm: Option[ConfirmationObject] = None) extends BlockElement @@ -182,10 +182,19 @@ case class UserSelectElement(placeholder: PlainTextObject, action_id: String, initial_user: Option[String] = None, confirm: Option[ConfirmationObject] = None) - extends BlockElement { + extends BlockElement + with InputBlockElement { override val `type`: String = "users_select" } +case class MultiUsersSelectElement( + placeholder: PlainTextObject, + action_id: String +) extends BlockElement + with InputBlockElement { + override val `type`: String = "multi_users_select" +} + case class ChannelSelectElement(placeholder: PlainTextObject, action_id: String, initial_channel: Option[String] = None, @@ -204,6 +213,18 @@ case class ConversationSelectElement(placeholder: PlainTextObject, override val `type`: String = "conversations_select" } +case class MultiConversationsSelectElement(placeholder: PlainTextObject, + action_id: String, + initial_conversations: Option[List[String]] = None, + default_to_current_conversation: Option[Boolean] = None, + confirm: Option[ConfirmationObject] = None, + max_selected_items: Option[Int] = None, + response_url_enabled: Option[Boolean] = None) + extends BlockElement + with InputBlockElement { + override val `type`: String = "multi_conversations_select" +} + case class OverflowElement(action_id: String, options: Seq[OptionObject], confirm: Option[ConfirmationObject] = None) extends BlockElement { override val `type`: String = "overflow" @@ -225,29 +246,33 @@ object BlockElement { implicit val optionGrpObjFmt = deriveCodec[OptionGroupObject] implicit val confirmObjFmt = deriveCodec[ConfirmationObject] - implicit val eitherOptFmt = eitherObjectFormat[OptionObject, OptionGroupObject]("text", "label") - implicit val buttonElementFmt = deriveCodec[ButtonElement] - implicit val imageElementFmt = deriveCodec[ImageElement] - implicit val staticMenuElementFmt = deriveCodec[StaticSelectElement] - implicit val extMenuElementFmt = deriveCodec[ExternalSelectElement] - implicit val userMenuElementFmt = deriveCodec[UserSelectElement] - implicit val channelMenuElementFmt = deriveCodec[ChannelSelectElement] - implicit val conversationMenuElementFmt = deriveCodec[ConversationSelectElement] - implicit val overflowElementFmt = deriveCodec[OverflowElement] - implicit val datePickerElementFmt = deriveCodec[DatePickerElement] + implicit val eitherOptFmt = eitherObjectFormat[OptionObject, OptionGroupObject]("text", "label") + implicit val buttonElementFmt = deriveCodec[ButtonElement] + implicit val imageElementFmt = deriveCodec[ImageElement] + implicit val staticMenuElementFmt = deriveCodec[StaticSelectElement] + implicit val extMenuElementFmt = deriveCodec[ExternalSelectElement] + implicit val userMenuElementFmt = deriveCodec[UserSelectElement] + implicit val multiUsersSelectElementFmt = deriveCodec[MultiUsersSelectElement] + implicit val channelMenuElementFmt = deriveCodec[ChannelSelectElement] + implicit val conversationMenuElementFmt = deriveCodec[ConversationSelectElement] + implicit val multiConversationMenuElementFmt = deriveCodec[MultiConversationsSelectElement] + implicit val overflowElementFmt = deriveCodec[OverflowElement] + implicit val datePickerElementFmt = deriveCodec[DatePickerElement] private val elemWrites = new Encoder[BlockElement] { def apply(element: BlockElement): Json = { val json = element match { - case elem: ButtonElement => elem.asJson - case elem: ImageElement => elem.asJson - case elem: StaticSelectElement => elem.asJson - case elem: ExternalSelectElement => elem.asJson - case elem: UserSelectElement => elem.asJson - case elem: ChannelSelectElement => elem.asJson - case elem: ConversationSelectElement => elem.asJson - case elem: OverflowElement => elem.asJson - case elem: DatePickerElement => elem.asJson + case elem: ButtonElement => elem.asJson + case elem: ImageElement => elem.asJson + case elem: StaticSelectElement => elem.asJson + case elem: ExternalSelectElement => elem.asJson + case elem: UserSelectElement => elem.asJson + case elem: MultiUsersSelectElement => elem.asJson + case elem: ChannelSelectElement => elem.asJson + case elem: ConversationSelectElement => elem.asJson + case elem: MultiConversationsSelectElement => elem.asJson + case elem: OverflowElement => elem.asJson + case elem: DatePickerElement => elem.asJson } Json.obj("type" -> element.`type`.asJson).deepMerge(json) } @@ -258,16 +283,18 @@ object BlockElement { for { value <- c.downField("type").as[String] result <- value match { - case "button" => c.as[ButtonElement] - case "image" => c.as[ImageElement] - case "static_select" => c.as[StaticSelectElement] - case "external_select" => c.as[ExternalSelectElement] - case "users_select" => c.as[UserSelectElement] - case "conversations_select" => c.as[ConversationSelectElement] - case "channels_select" => c.as[ChannelSelectElement] - case "overflow" => c.as[OverflowElement] - case "datepicker" => c.as[DatePickerElement] - case other => Left(DecodingFailure(s"Invalid element type: $other", List.empty)) + case "button" => c.as[ButtonElement] + case "image" => c.as[ImageElement] + case "static_select" => c.as[StaticSelectElement] + case "external_select" => c.as[ExternalSelectElement] + case "users_select" => c.as[UserSelectElement] + case "multi_users_select" => c.as[MultiUsersSelectElement] + case "conversations_select" => c.as[ConversationSelectElement] + case "multi_conversations_select" => c.as[MultiConversationsSelectElement] + case "channels_select" => c.as[ChannelSelectElement] + case "overflow" => c.as[OverflowElement] + case "datepicker" => c.as[DatePickerElement] + case other => Left(DecodingFailure(s"Invalid element type: $other", List.empty)) } } yield result } @@ -280,10 +307,13 @@ object InputBlockElement { implicit val encoder: Encoder[InputBlockElement] = Encoder.AsObject.instance[InputBlockElement] { ibe => val json = ibe match { - case i: PlainTextInput => i.asJsonObject - case i: StaticSelectElement => i.asJsonObject - case i: DatePickerElement => i.asJsonObject - case i: ConversationSelectElement => i.asJsonObject + case i: PlainTextInput => i.asJsonObject + case i: StaticSelectElement => i.asJsonObject + case i: DatePickerElement => i.asJsonObject + case i: ConversationSelectElement => i.asJsonObject + case i: MultiConversationsSelectElement => i.asJsonObject + case i: UserSelectElement => i.asJsonObject + case i: MultiUsersSelectElement => i.asJsonObject } json.add("type", ibe.`type`.asJson) @@ -293,10 +323,14 @@ object InputBlockElement { implicit val decoder: Decoder[InputBlockElement] = Decoder.instance[InputBlockElement] { c => typeDecoder(c).flatMap { - case "plain_text_input" => c.as[PlainTextInput] - case "static_select" => c.as[StaticSelectElement] - case "datepicker" => c.as[DatePickerElement] - case "conversations_select" => c.as[ConversationSelectElement] + case "plain_text_input" => c.as[PlainTextInput] + case "static_select" => c.as[StaticSelectElement] + case "datepicker" => c.as[DatePickerElement] + case "conversations_select" => c.as[ConversationSelectElement] + case "multi_conversations_select" => c.as[MultiConversationsSelectElement] + case "users_select" => c.as[UserSelectElement] + case "multi_users_select" => c.as[MultiUsersSelectElement] + case t => Left(DecodingFailure(s"Unknown input block element type $t", c.history)) } } diff --git a/client/src/main/scala/com/github/dapperware/slack/models/ViewPayload.scala b/client/src/main/scala/com/github/dapperware/slack/models/ViewPayload.scala index d126472..de6ebfc 100644 --- a/client/src/main/scala/com/github/dapperware/slack/models/ViewPayload.scala +++ b/client/src/main/scala/com/github/dapperware/slack/models/ViewPayload.scala @@ -1,9 +1,10 @@ package com.github.dapperware.slack.models -import io.circe.Codec +import cats.Applicative.ops.toAllApplicativeOps +import io.circe.{ Codec, Decoder } import io.circe.generic.semiauto._ -case class View( +final case class View( id: String, team_id: String, `type`: String, @@ -25,10 +26,10 @@ case class View( ) object View { - implicit val viewCodec: Codec[View] = deriveCodec[View] + implicit val viewCodec: Decoder[View] = deriveDecoder[View] } -case class ViewPayload( +final case class ViewPayload( `type`: String, title: PlainTextObject, blocks: Seq[Block], @@ -42,17 +43,74 @@ case class ViewPayload( submit_disabled: Option[Boolean] = None ) -case class ViewState(values: Map[String, Map[String, ViewStateValue]]) { +final case class ViewState(values: Map[String, Map[String, ViewStateValue]]) { def getValue(block: String, action: String): Option[ViewStateValue] = values.get(block).flatMap(_.get(action)) } -case class ViewStateValue(`type`: String, value: String) +sealed trait ViewStateValue { + def `type`: String +} + +case class PlainTextValue(value: Option[String]) extends ViewStateValue { + val `type` = "plain_text_input" +} + +case class SelectedOption(text: PlainTextObject, value: String) + +case class StaticSelectValue(selected_option: Option[SelectedOption]) extends ViewStateValue { + override val `type`: String = "static_select" +} + +case class MultiStaticSelectValue(selected_options: List[SelectedOption]) extends ViewStateValue { + override val `type`: String = "multi_static_select" +} + +case class MultiConversationsValue(selected_conversations: List[String]) extends ViewStateValue { + override val `type`: String = "multi_conversations_select" +} + +case class ConversationsSelectValue(selected_conversation: Option[String]) extends ViewStateValue { + override val `type`: String = "conversations_select" +} + +case class DatepickerValue(selected_date: Option[String]) extends ViewStateValue { + override val `type`: String = "datepicker" +} + +case class MultiUsersSelectValue(selected_users: List[String]) extends ViewStateValue { + override val `type`: String = "multi_users_select" +} + +case class UsersSelectValue(selected_user: Option[String]) extends ViewStateValue { + override val `type`: String = "users_select" +} object ViewState { - implicit val viewStateChildCodec: Codec.AsObject[ViewStateValue] = deriveCodec[ViewStateValue] + private val typeDecoder: Decoder[String] = Decoder.decodeString.at("type") + private implicit val selectedOptionDecoder: Decoder[SelectedOption] = deriveDecoder[SelectedOption] + private implicit val plainTextDecoder: Decoder[PlainTextValue] = deriveDecoder + private implicit val staticSelectDecoder: Decoder[StaticSelectValue] = deriveDecoder + private implicit val multiStaticSelectDecoder: Decoder[MultiStaticSelectValue] = deriveDecoder + private implicit val datepickerDecoder: Decoder[DatepickerValue] = deriveDecoder + private implicit val multiUserSelectDecoder: Decoder[MultiUsersSelectValue] = deriveDecoder + private implicit val usersSelectDecoder: Decoder[UsersSelectValue] = deriveDecoder + private implicit val multiConversationsSelectDecoder: Decoder[MultiConversationsValue] = deriveDecoder + private implicit val conversationsSelectDecoder: Decoder[ConversationsSelectValue] = deriveDecoder + + implicit val viewStateChildDecoder: Decoder[ViewStateValue] = typeDecoder.flatMap { + case "plain_text_input" => Decoder[PlainTextValue].widen + case "static_select" => Decoder[StaticSelectValue].widen + case "multi_static_select" => Decoder[MultiStaticSelectValue].widen + case "datepicker" => Decoder[DatepickerValue].widen + case "multi_users_select" => Decoder[MultiUsersSelectValue].widen + case "users_select" => Decoder[UsersSelectValue].widen + case "multi_conversations_select" => Decoder[MultiConversationsValue].widen + case "conversations_select" => Decoder[ConversationsSelectValue].widen + case t => Decoder.failedWithMessage(s"Unknown view type $t is not supported") + } - implicit val viewStateDecoder: Codec.AsObject[ViewState] = deriveCodec[ViewState] + implicit val viewStateDecoder: Decoder[ViewState] = deriveDecoder[ViewState] } object ViewPayload { diff --git a/client/src/test/resources/payloads/kitchen_sink_view_submission.json b/client/src/test/resources/payloads/kitchen_sink_view_submission.json new file mode 100644 index 0000000..085c2c5 --- /dev/null +++ b/client/src/test/resources/payloads/kitchen_sink_view_submission.json @@ -0,0 +1,484 @@ +{ + "id": "V1234567890", + "team_id": "T5LBTFQMC", + "type": "modal", + "blocks": [ + { + "type": "section", + "block_id": "5oyRw", + "text": { + "type": "mrkdwn", + "text": "*Hi !* Here's how I can help you:", + "verbatim": false + } + }, + { + "type": "divider", + "block_id": "ci14" + }, + { + "type": "section", + "block_id": "njqX", + "text": { + "type": "mrkdwn", + "text": ":calendar: *Create event*\nCreate a new event", + "verbatim": false + }, + "accessory": { + "type": "button", + "action_id": "Yq59z", + "style": "primary", + "text": { + "type": "plain_text", + "text": "Create event", + "emoji": true + }, + "value": "click_me_123" + } + }, + { + "type": "section", + "block_id": "Dtd", + "text": { + "type": "mrkdwn", + "text": ":clipboard: *List of events*\nChoose from different event lists", + "verbatim": false + }, + "accessory": { + "type": "static_select", + "action_id": "Aho", + "placeholder": { + "type": "plain_text", + "text": "Choose list", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "My events", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "All events", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Event invites", + "emoji": true + }, + "value": "value-1" + } + ] + } + }, + { + "type": "section", + "block_id": "FdXk", + "text": { + "type": "mrkdwn", + "text": ":gear: *Settings*\nManage your notifications and team settings", + "verbatim": false + }, + "accessory": { + "type": "static_select", + "action_id": "UqxC2", + "placeholder": { + "type": "plain_text", + "text": "Edit settings", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Notifications", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Team settings", + "emoji": true + }, + "value": "value-1" + } + ] + } + }, + { + "type": "actions", + "block_id": "YShg", + "elements": [ + { + "type": "button", + "action_id": "Q0tSJ", + "text": { + "type": "plain_text", + "text": "Send feedback", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "action_id": "V6F", + "text": { + "type": "plain_text", + "text": "FAQs", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "input", + "block_id": "EqC", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": true, + "element": { + "type": "plain_text_input", + "action_id": "plain_text_input-action", + "dispatch_action_config": { + "trigger_actions_on": [ + "on_enter_pressed" + ] + } + } + }, + { + "type": "input", + "block_id": "nYc", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": true, + "element": { + "type": "plain_text_input", + "action_id": "plain_text_input-action", + "dispatch_action_config": { + "trigger_actions_on": [ + "on_character_entered" + ] + } + } + }, + { + "type": "input", + "block_id": "WVv", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": false, + "element": { + "type": "plain_text_input", + "action_id": "plain_text_input-action", + "multiline": true, + "dispatch_action_config": { + "trigger_actions_on": [ + "on_enter_pressed" + ] + } + } + }, + { + "type": "input", + "block_id": "p9m", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": false, + "element": { + "type": "plain_text_input", + "action_id": "plain_text_input-action", + "dispatch_action_config": { + "trigger_actions_on": [ + "on_enter_pressed" + ] + } + } + }, + { + "type": "input", + "block_id": "Thr7", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": false, + "element": { + "type": "multi_users_select", + "action_id": "multi_users_select-action", + "placeholder": { + "type": "plain_text", + "text": "Select users", + "emoji": true + } + } + }, + { + "type": "input", + "block_id": "9HypQ", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": false, + "element": { + "type": "static_select", + "action_id": "static_select-action", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "input", + "block_id": "b3Vh", + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + }, + "optional": false, + "dispatch_action": false, + "element": { + "type": "datepicker", + "action_id": "datepicker-action", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + }, + { + "type": "section", + "block_id": "Txov8", + "text": { + "type": "mrkdwn", + "text": "Test block with multi conversations select", + "verbatim": false + }, + "accessory": { + "type": "multi_conversations_select", + "action_id": "multi_conversations_select-action", + "placeholder": { + "type": "plain_text", + "text": "Select conversations", + "emoji": true + } + } + }, + { + "type": "section", + "block_id": "IWW2", + "text": { + "type": "mrkdwn", + "text": "Pick an item from the dropdown list", + "verbatim": false + }, + "accessory": { + "type": "static_select", + "action_id": "static_select-action", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + } + } + ], + "private_metadata": "", + "callback_id": "", + "state": { + "values": { + "Dtd": { + "Aho": { + "type": "static_select", + "selected_option": null + } + }, + "FdXk": { + "UqxC2": { + "type": "static_select", + "selected_option": null + } + }, + "EqC": { + "plain_text_input-action": { + "type": "plain_text_input", + "value": null + } + }, + "nYc": { + "plain_text_input-action": { + "type": "plain_text_input", + "value": null + } + }, + "WVv": { + "plain_text_input-action": { + "type": "plain_text_input", + "value": null + } + }, + "p9m": { + "plain_text_input-action": { + "type": "plain_text_input", + "value": null + } + }, + "Thr7": { + "multi_users_select-action": { + "type": "multi_users_select", + "selected_users": [ + "U5K0V0TK3", + "U5KJZJSMA" + ] + } + }, + "9HypQ": { + "static_select-action": { + "type": "static_select", + "selected_option": null + } + }, + "b3Vh": { + "datepicker-action": { + "type": "datepicker", + "selected_date": "1990-04-28" + } + }, + "Txov8": { + "multi_conversations_select-action": { + "type": "multi_conversations_select", + "selected_conversations": [ + "C01H925K0HK" + ] + } + }, + "IWW2": { + "static_select-action": { + "type": "static_select", + "selected_option": { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + } + } + } + } + }, + "hash": "1611900766.EToIJMjF", + "title": { + "type": "plain_text", + "text": "App menu", + "emoji": true + }, + "clear_on_close": false, + "notify_on_close": false, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "previous_view_id": null, + "root_view_id": "V1234567890", + "app_id": "A02", + "external_id": "", + "app_installed_team_id": "T5LBTFQMC", + "bot_id": "B00" +} \ No newline at end of file diff --git a/client/src/test/scala/slack/api/SlackViewSpec.scala b/client/src/test/scala/slack/api/SlackViewSpec.scala index 169e493..0dd25b3 100644 --- a/client/src/test/scala/slack/api/SlackViewSpec.scala +++ b/client/src/test/scala/slack/api/SlackViewSpec.scala @@ -5,11 +5,15 @@ import com.github.dapperware.slack.models.{ InputBlock, PlainTextInput, PlainTextObject, + View, ViewPayload } import io.circe.parser import zio.test.Assertion.{ equalTo, isRight } import zio.test._ +import zio.{ UIO, ZManaged } + +import scala.io.Source object SlackViewSpec extends DefaultRunnableSpec { override def spec: ZSpec[_root_.zio.test.environment.TestEnvironment, Any] = @@ -94,6 +98,14 @@ object SlackViewSpec extends DefaultRunnableSpec { ) ) ) + }, + testM("deserialize kitchen sink submission") { + val body = ZManaged + .fromAutoCloseable(UIO(Source.fromResource("payloads/kitchen_sink_view_submission.json"))) + .map(_.mkString) + .useNow + + assertM(body.map(parser.parse(_).flatMap(_.as[View])))(isRight) } ) }