@@ -291,6 +291,26 @@ trait CatsEffectAssertions { self: Assertions =>
291291 Sync [F ].raiseError[T ](e)
292292 }
293293
294+ private def mapOrFailF [F [_], A , B ](
295+ io : F [A ],
296+ pf : PartialFunction [A , B ],
297+ clue : => Any
298+ )(implicit F : Sync [F ], loc : Location ): F [B ] =
299+ F .flatMap(io) { a =>
300+ // It could be just "case `pf`(b) => F.pure(b)" but 2.12 doesn't define `unapply` for `PartialFunction`.
301+ pf.andThen(F .pure[B ])
302+ .applyOrElse[A , F [B ]](
303+ a,
304+ aa =>
305+ F .raiseError(
306+ new FailException (
307+ s " ${munitPrint(clue)}, value obtained: $aa" ,
308+ location = loc
309+ )
310+ )
311+ )
312+ }
313+
294314 implicit class MUnitCatsAssertionsForIOOps [A ](io : IO [A ]) {
295315
296316 /** Asserts that this effect returns an expected value.
@@ -335,6 +355,46 @@ trait CatsEffectAssertions { self: Assertions =>
335355 ): IO [Unit ] =
336356 assertIOBoolean(io.map(pred), clue)
337357
358+ /** Maps a value from this effect with a given `PartialFunction` or fails if the value doesn't
359+ * match. Then the mapped value can be used for further processing or validation.
360+ *
361+ * This method can come in handy in complex validation scenarios where multi-step assertions
362+ * are necessary.
363+ *
364+ * @example
365+ * {{{
366+ * case class Response(status: Int, body: IO[Array[Byte]])
367+ *
368+ * def decodeResponseBytes(bytes: IO[Array[Byte]]): IO[String] =
369+ * bytes.map(String.fromBytes(_))
370+ *
371+ * val response: IO[Response] =
372+ * IO.pure(Response(
373+ * status = 200,
374+ * body = IO {
375+ * "<expected response body>".getBytes("UTF-8")
376+ * }
377+ * ))
378+ *
379+ * response
380+ * // First, check if the response has the expected status,
381+ * // then pass it over to the next step for further processing.
382+ * .mapOrFail { case Response(200, body) => body }
383+ * // Decode the response body in order to prepare for the final check.
384+ * .flatMap(decodeResponseBytes)
385+ * // Make sure that the response has the expected content.
386+ * .assertEquals("<expected response body>")
387+ * }}}
388+ *
389+ * @param pf
390+ * a partial function that matches the value obtained from the effect
391+ */
392+ def mapOrFail [B ](
393+ pf : PartialFunction [A , B ],
394+ clue : => Any = " value didn't match any of the defined cases"
395+ )(implicit loc : Location ): IO [B ] =
396+ mapOrFailF(io, pf, clue)
397+
338398 /** Intercepts a `Throwable` being thrown inside this effect.
339399 *
340400 * @example
@@ -433,6 +493,46 @@ trait CatsEffectAssertions { self: Assertions =>
433493 ): SyncIO [Unit ] =
434494 assertSyncIOBoolean(io.map(pred), clue)
435495
496+ /** Maps a value from this effect with a given `PartialFunction` or fails if the value doesn't
497+ * match. Then the mapped value can be used for further processing or validation.
498+ *
499+ * This method can come in handy in complex validation scenarios where multi-step assertions
500+ * are necessary.
501+ *
502+ * @example
503+ * {{{
504+ * case class Response(status: Int, body: SyncIO[Array[Byte]])
505+ *
506+ * def decodeResponseBytes(bytes: SyncIO[Array[Byte]]): SyncIO[String] =
507+ * bytes.map(String.fromBytes(_))
508+ *
509+ * val response: SyncIO[Response] =
510+ * SyncIO.pure(Response(
511+ * status = 200,
512+ * body = SyncIO {
513+ * "<expected response body>".getBytes("UTF-8")
514+ * }
515+ * ))
516+ *
517+ * response
518+ * // First, check if the response has the expected status,
519+ * // then pass it over to the next step for further processing.
520+ * .mapOrFail { case Response(200, body) => body }
521+ * // Decode the response body in order to prepare for the final check.
522+ * .flatMap(decodeResponseBytes)
523+ * // Make sure that the response has the expected content.
524+ * .assertEquals("<expected response body>")
525+ * }}}
526+ *
527+ * @param pf
528+ * a partial function that matches the value obtained from the effect
529+ */
530+ def mapOrFail [B ](
531+ pf : PartialFunction [A , B ],
532+ clue : => Any = " value didn't match any of the defined cases"
533+ )(implicit loc : Location ): SyncIO [B ] =
534+ mapOrFailF(io, pf, clue)
535+
436536 /** Intercepts a `Throwable` being thrown inside this effect.
437537 *
438538 * @example
0 commit comments