Skip to content

Commit 4fc8379

Browse files
chore: apply comments from #26 and #27 to existing code in SourceOps.scala (#28)
The following comments were applied: * use supervised instead of scoped * use toThrowable instead of ad-hoc exception creation * don't catch exceptions in headOption * also fixed comments about the source and receive function
1 parent 2cde6cd commit 4fc8379

File tree

3 files changed

+51
-42
lines changed

3 files changed

+51
-42
lines changed

core/src/main/scala/ox/channels/SourceOps.scala

+32-31
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import ox.*
55
import java.util.concurrent.{CountDownLatch, Semaphore}
66
import scala.collection.{IterableOnce, mutable}
77
import scala.concurrent.duration.FiniteDuration
8-
import scala.util.Try
98

109
trait SourceOps[+T] { this: Source[T] =>
1110
// view ops (lazy)
@@ -46,7 +45,7 @@ trait SourceOps[+T] { this: Source[T] =>
4645
* import ox.*
4746
* import ox.channels.Source
4847
*
49-
* scoped {
48+
* supervised {
5049
* Source.empty[String].intersperse(", ").toList // List()
5150
* Source.fromValues("foo").intersperse(", ").toList // List(foo)
5251
* Source.fromValues("foo", "bar").intersperse(", ").toList // List(foo, ", ", bar)
@@ -71,7 +70,7 @@ trait SourceOps[+T] { this: Source[T] =>
7170
* import ox.*
7271
* import ox.channels.Source
7372
*
74-
* scoped {
73+
* supervised {
7574
* Source.empty[String].intersperse("[", ", ", "]").toList // List([, ])
7675
* Source.fromValues("foo").intersperse("[", ", ", "]").toList // List([, foo, ])
7776
* Source.fromValues("foo", "bar").intersperse("[", ", ", "]").toList // List([, foo, ", ", bar, ])
@@ -210,7 +209,7 @@ trait SourceOps[+T] { this: Source[T] =>
210209
* import ox.*
211210
* import ox.channels.Source
212211
*
213-
* scoped {
212+
* supervised {
214213
* Source.empty[Int].takeWhile(_ > 3).toList // List()
215214
* Source.fromValues(1, 2, 3).takeWhile(_ < 3).toList // List(1, 2)
216215
* Source.fromValues(3, 2, 1).takeWhile(_ < 3).toList // List()
@@ -228,7 +227,7 @@ trait SourceOps[+T] { this: Source[T] =>
228227
* import ox.*
229228
* import ox.channels.Source
230229
*
231-
* scoped {
230+
* supervised {
232231
* Source.empty[Int].drop(1).toList // List()
233232
* Source.fromValues(1, 2, 3).drop(1).toList // List(2 ,3)
234233
* Source.fromValues(1).drop(2).toList // List()
@@ -303,7 +302,7 @@ trait SourceOps[+T] { this: Source[T] =>
303302
* import ox.*
304303
* import ox.channels.Source
305304
*
306-
* scoped {
305+
* supervised {
307306
* Source.empty[Int].zipAll(Source.empty[String], -1, "foo").toList // List()
308307
* Source.empty[Int].zipAll(Source.fromValues("a"), -1, "foo").toList // List((-1, "a"))
309308
* Source.fromValues(1).zipAll(Source.empty[String], -1, "foo").toList // List((1, "foo"))
@@ -355,7 +354,7 @@ trait SourceOps[+T] { this: Source[T] =>
355354
* import ox.*
356355
* import ox.channels.Source
357356
*
358-
* scoped {
357+
* supervised {
359358
* val s1 = Source.fromValues(1, 2, 3, 4, 5, 6, 7)
360359
* val s2 = Source.fromValues(10, 20, 30, 40)
361360
* s1.interleave(s2, segmentSize = 2).toList
@@ -433,7 +432,7 @@ trait SourceOps[+T] { this: Source[T] =>
433432
* import ox.*
434433
* import ox.channels.Source
435434
*
436-
* scoped {
435+
* supervised {
437436
* val s = Source.fromValues(1, 2, 3, 4, 5)
438437
* s.mapStateful(() => 0)((sum, element) => (sum + element, sum), Some.apply)
439438
* }
@@ -476,7 +475,7 @@ trait SourceOps[+T] { this: Source[T] =>
476475
* import ox.*
477476
* import ox.channels.Source
478477
*
479-
* scoped {
478+
* supervised {
480479
* val s = Source.fromValues(1, 2, 2, 3, 2, 4, 3, 1, 5)
481480
* // deduplicate the values
482481
* s.mapStatefulConcat(() => Set.empty[Int])((s, e) => (s + e, Option.unless(s.contains(e))(e)))
@@ -515,56 +514,58 @@ trait SourceOps[+T] { this: Source[T] =>
515514
}
516515
c
517516

518-
/** Returns the first element from this source wrapped in `Some` or `None` when the source is empty or fails during the receive operation.
519-
* Note that `headOption` is not an idempotent operation on source as it receives elements from it.
517+
/** Returns the first element from this source wrapped in [[Some]] or [[None]] when this source is empty. Note that `headOption` is not an
518+
* idempotent operation on source as it receives elements from it.
520519
*
521520
* @return
522-
* A `Some(first element)` if source is not empty or None` otherwise.
521+
* A `Some(first element)` if source is not empty or `None` otherwise.
522+
* @throws ChannelClosedException.Error
523+
* When receiving an element from this source fails.
523524
* @example
524525
* {{{
525526
* import ox.*
526527
* import ox.channels.Source
527528
*
528-
* scoped {
529+
* supervised {
529530
* Source.empty[Int].headOption() // None
530531
* val s = Source.fromValues(1, 2)
531532
* s.headOption() // Some(1)
532533
* s.headOption() // Some(2)
533534
* }
534535
* }}}
535536
*/
536-
def headOption(): Option[T] = Try(head()).toOption
537+
def headOption(): Option[T] =
538+
supervised {
539+
receive() match
540+
case ChannelClosed.Done => None
541+
case e: ChannelClosed.Error => throw e.toThrowable
542+
case t: T @unchecked => Some(t)
543+
}
537544

538-
/** Returns the first element from this source or throws `NoSuchElementException` when the source is empty or `receive()` operation fails
539-
* without error. In case when the `receive()` operation fails with exception that exception is re-thrown. Note that `headOption` is not
540-
* an idempotent operation on source as it receives elements from it.
545+
/** Returns the first element from this source or throws [[NoSuchElementException]] when this source is empty. In case when receiving an
546+
* element fails with exception then [[ChannelClosedException.Error]] is thrown. Note that `head` is not an idempotent operation on
547+
* source as it receives elements from it.
541548
*
542549
* @return
543550
* A first element if source is not empty or throws otherwise.
544551
* @throws NoSuchElementException
545-
* When source is empty or `receive()` failed without error.
546-
* @throws exception
547-
* When `receive()` failed with exception then this exception is re-thrown.
552+
* When this source is empty.
553+
* @throws ChannelClosedException.Error
554+
* When receiving an element from this source fails.
548555
* @example
549556
* {{{
550557
* import ox.*
551558
* import ox.channels.Source
552559
*
553-
* scoped {
554-
* Source.empty[Int].head() // throws NoSuchElementException("cannot obtain head from an empty source")
560+
* supervised {
561+
* Source.empty[Int].head() // throws NoSuchElementException("cannot obtain head element from an empty source")
555562
* val s = Source.fromValues(1, 2)
556563
* s.head() // 1
557564
* s.head() // 2
558565
* }
559566
* }}}
560567
*/
561-
def head(): T =
562-
supervised {
563-
receive() match
564-
case ChannelClosed.Done => throw new NoSuchElementException("cannot obtain head from an empty source")
565-
case ChannelClosed.Error(r) => throw r.getOrElse(new NoSuchElementException("getting head failed"))
566-
case t: T @unchecked => t
567-
}
568+
def head(): T = headOption().getOrElse(throw new NoSuchElementException("cannot obtain head element from an empty source"))
568569

569570
/** Sends elements to the returned channel limiting the throughput to specific number of elements (evenly spaced) per time unit. Note that
570571
* the element's `receive()` time is included in the resulting throughput. For instance having `throttle(1, 1.second)` and `receive()`
@@ -645,7 +646,7 @@ trait SourceOps[+T] { this: Source[T] =>
645646
* @return
646647
* A last element if source is not empty or throws otherwise.
647648
* @throws NoSuchElementException
648-
* When source is empty.
649+
* When this source is empty.
649650
* @throws ChannelClosedException.Error
650651
* When receiving an element from this source fails.
651652
* @example
@@ -822,7 +823,7 @@ trait SourceCompanionOps:
822823
* import ox.*
823824
* import ox.channels.Source
824825
*
825-
* scoped {
826+
* supervised {
826827
* val s1 = Source.fromValues(1, 2, 3, 4, 5, 6, 7, 8)
827828
* val s2 = Source.fromValues(10, 20, 30)
828829
* val s3 = Source.fromValues(100, 200, 300, 400, 500)

core/src/test/scala/ox/channels/SourceOpsHeadOptionTest.scala

+12-4
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@ class SourceOpsHeadOptionTest extends AnyFlatSpec with Matchers with OptionValue
1212
Source.empty[Int].headOption() shouldBe None
1313
}
1414

15-
it should "return None for the failed source" in supervised {
16-
Source
17-
.failed(new RuntimeException("source is broken"))
18-
.headOption() shouldBe None
15+
it should "throw ChannelClosedException.Error with exception and message that was thrown during retrieval" in supervised {
16+
the[ChannelClosedException.Error] thrownBy {
17+
Source
18+
.failed(new RuntimeException("source is broken"))
19+
.headOption()
20+
} should have message "java.lang.RuntimeException: source is broken"
21+
}
22+
23+
it should "throw ChannelClosedException.Error for source failed without exception" in supervised {
24+
the[ChannelClosedException.Error] thrownBy {
25+
Source.failedWithoutReason[Int]().headOption()
26+
}
1927
}
2028

2129
it should "return Some element for the non-empty source" in supervised {

core/src/test/scala/ox/channels/SourceOpsHeadTest.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,21 @@ class SourceOpsHeadTest extends AnyFlatSpec with Matchers {
1010
it should "throw NoSuchElementException for the empty source" in supervised {
1111
the[NoSuchElementException] thrownBy {
1212
Source.empty[Int].head()
13-
} should have message "cannot obtain head from an empty source"
13+
} should have message "cannot obtain head element from an empty source"
1414
}
1515

16-
it should "re-throw exception that was thrown during element retrieval" in supervised {
17-
the[RuntimeException] thrownBy {
16+
it should "throw ChannelClosedException.Error with exception and message that was thrown during retrieval" in supervised {
17+
the[ChannelClosedException.Error] thrownBy {
1818
Source
1919
.failed(new RuntimeException("source is broken"))
2020
.head()
21-
} should have message "source is broken"
21+
} should have message "java.lang.RuntimeException: source is broken"
2222
}
2323

24-
it should "throw NoSuchElementException for source failed without exception" in supervised {
25-
the[NoSuchElementException] thrownBy {
24+
it should "throw ChannelClosedException.Error for source failed without exception" in supervised {
25+
the[ChannelClosedException.Error] thrownBy {
2626
Source.failedWithoutReason[Int]().head()
27-
} should have message "getting head failed"
27+
}
2828
}
2929

3030
it should "return first value from non empty source" in supervised {

0 commit comments

Comments
 (0)