Skip to content

Commit 23b8bfe

Browse files
committed
Add for comprehension support to UsingUtils.
- Introduced implicit classes and wrappers to enable `foreach`, `map`, `flatMap`, and `withFilter` for resources in `UsingUtils`. - Updated tests to verify resource management and exception handling in `for` comprehensions.
1 parent c5fe9ff commit 23b8bfe

2 files changed

Lines changed: 304 additions & 1 deletion

File tree

pramen/core/src/main/scala/za/co/absa/pramen/core/utils/UsingUtils.scala

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,63 @@ object UsingUtils {
6464
}
6565
}
6666
}
67+
68+
/**
69+
* Implicits implementing resource management via for comprehension. Example usage:
70+
* {{{
71+
* import za.co.absa.pramen.core.utils.UsingUtils.Implicits._
72+
*
73+
* for {
74+
* res1 <- new AutoCloseableResource()
75+
* res2 <- new AutoCloseableResource()
76+
* } {
77+
* // Perform operations with the resource
78+
* }
79+
* }}}
80+
*
81+
* or
82+
*
83+
* {{{
84+
* import za.co.absa.pramen.core.utils.UsingUtils.Implicits._
85+
*
86+
* val result = for {
87+
* res1 <- new AutoCloseableResource()
88+
* res2 <- new AutoCloseableResource()
89+
* } yield {
90+
* // Perform operations with the resource, and return a value
91+
* }
92+
* }}}
93+
*/
94+
object Implicits {
95+
implicit class ResourceWrapper[T <: AutoCloseable](private val resource: T) {
96+
def foreach(f: T => Unit): Unit = using(resource)(f)
97+
98+
def map[U](body: T => U): U = using(resource)(body)
99+
100+
def flatMap[U](body: T => U): U = using(resource)(body)
101+
102+
def withFilter(p: T => Boolean): FilteredResourceWrapper[T] = new FilteredResourceWrapper[T](resource, p)
103+
}
104+
}
105+
106+
final class FilteredResourceWrapper[T <: AutoCloseable](private val resource: T,
107+
private val p: T => Boolean) {
108+
def foreach(f: T => Unit): Unit =
109+
using(resource) { r =>
110+
if (p(r)) f(r) else ()
111+
}
112+
113+
def map[U](body: T => U): U =
114+
using(resource) { r =>
115+
if (p(r)) body(r) else throw new NoSuchElementException("withFilter predicate is false")
116+
}
117+
118+
def flatMap[U](body: T => U): U =
119+
using(resource) { r =>
120+
if (p(r)) body(r) else throw new NoSuchElementException("withFilter predicate is false")
121+
}
122+
123+
def withFilter(p2: T => Boolean): FilteredResourceWrapper[T] =
124+
new FilteredResourceWrapper[T](resource, r => p(r) && p2(r))
125+
}
67126
}

pramen/core/src/test/scala/za/co/absa/pramen/core/tests/utils/UsingUtilsSuite.scala

Lines changed: 245 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import za.co.absa.pramen.core.mocks.AutoCloseableSpy
2121
import za.co.absa.pramen.core.utils.UsingUtils
2222

2323
class UsingUtilsSuite extends AnyWordSpec {
24+
import UsingUtils.Implicits._
25+
2426
"using with a single resource" should {
2527
"properly close the resource" in {
2628
var resource: AutoCloseableSpy = null
2729

28-
UsingUtils.using(new AutoCloseableSpy()) { res =>
30+
for (res <- new AutoCloseableSpy()) {
2931
resource = res
3032
res.dummyAction()
3133
}
@@ -181,6 +183,28 @@ class UsingUtilsSuite extends AnyWordSpec {
181183
assert(resource2.closeCallCount == 1)
182184
}
183185

186+
"work with for comprehension" in {
187+
var resource1: AutoCloseableSpy = null
188+
var resource2: AutoCloseableSpy = null
189+
190+
val result = for {
191+
res1 <- new AutoCloseableSpy()
192+
res2 <- new AutoCloseableSpy()
193+
} yield {
194+
resource1 = res1
195+
resource2 = res2
196+
res1.dummyAction()
197+
res2.dummyAction()
198+
100
199+
}
200+
201+
assert(result == 100)
202+
assert(resource1.actionCallCount == 1)
203+
assert(resource1.closeCallCount == 1)
204+
assert(resource2.actionCallCount == 1)
205+
assert(resource2.closeCallCount == 1)
206+
}
207+
184208
"properly close both resources when an inner one throws an exception during action and close" in {
185209
var resource1: AutoCloseableSpy = null
186210
var resource2: AutoCloseableSpy = null
@@ -211,6 +235,37 @@ class UsingUtilsSuite extends AnyWordSpec {
211235
assert(resource2.closeCallCount == 1)
212236
}
213237

238+
"properly close both resources when an inner one throws an exception during action and close (for comprehension)" in {
239+
var resource1: AutoCloseableSpy = null
240+
var resource2: AutoCloseableSpy = null
241+
var exceptionThrown = false
242+
243+
try {
244+
for {
245+
res1 <- new AutoCloseableSpy()
246+
res2 <- new AutoCloseableSpy(failAction = true, failClose = true)
247+
} {
248+
resource1 = res1
249+
resource2 = res2
250+
res1.dummyAction()
251+
res2.dummyAction()
252+
}
253+
} catch {
254+
case ex: Throwable =>
255+
exceptionThrown = true
256+
assert(ex.getMessage.contains("Failed during action"))
257+
val suppressed = ex.getSuppressed
258+
assert(suppressed.length == 1)
259+
assert(suppressed(0).getMessage.contains("Failed to close resource"))
260+
}
261+
262+
assert(exceptionThrown)
263+
assert(resource1.actionCallCount == 1)
264+
assert(resource1.closeCallCount == 1)
265+
assert(resource2.actionCallCount == 1)
266+
assert(resource2.closeCallCount == 1)
267+
}
268+
214269
"properly close both resources when an outer one throws an exception during action and close" in {
215270
var resource1: AutoCloseableSpy = null
216271
var resource2: AutoCloseableSpy = null
@@ -241,6 +296,37 @@ class UsingUtilsSuite extends AnyWordSpec {
241296
assert(resource2.closeCallCount == 1)
242297
}
243298

299+
"properly close both resources when an outer one throws an exception during action and close (for comprehension)" in {
300+
var resource1: AutoCloseableSpy = null
301+
var resource2: AutoCloseableSpy = null
302+
var exceptionThrown = false
303+
304+
try {
305+
for {
306+
res1 <- new AutoCloseableSpy(failAction = true, failClose = true)
307+
res2 <- new AutoCloseableSpy()
308+
} {
309+
resource1 = res1
310+
resource2 = res2
311+
res1.dummyAction()
312+
res2.dummyAction()
313+
}
314+
} catch {
315+
case ex: Throwable =>
316+
exceptionThrown = true
317+
assert(ex.getMessage.contains("Failed during action"))
318+
val suppressed = ex.getSuppressed
319+
assert(suppressed.length == 1)
320+
assert(suppressed(0).getMessage.contains("Failed to close resource"))
321+
}
322+
323+
assert(exceptionThrown)
324+
assert(resource1.actionCallCount == 1)
325+
assert(resource1.closeCallCount == 1)
326+
assert(resource2.actionCallCount == 0)
327+
assert(resource2.closeCallCount == 1)
328+
}
329+
244330
"properly close the outer resource when the inner one fails on create" in {
245331
var resource1: AutoCloseableSpy = null
246332
var resource2: AutoCloseableSpy = null
@@ -266,5 +352,163 @@ class UsingUtilsSuite extends AnyWordSpec {
266352
assert(resource1.closeCallCount == 1)
267353
assert(resource2 == null)
268354
}
355+
356+
"properly close the outer resource when the inner one fails on create (for comprehension)" in {
357+
val resource1: AutoCloseableSpy = new AutoCloseableSpy()
358+
var resource2: AutoCloseableSpy = null
359+
var exceptionThrown = false
360+
361+
try {
362+
resource1
363+
.flatMap(res1 =>
364+
new AutoCloseableSpy(failCreate = true)
365+
.map(res2 => {
366+
resource2 = res2
367+
res1.dummyAction()
368+
res2.dummyAction()
369+
})
370+
)
371+
} catch {
372+
case ex: Throwable =>
373+
exceptionThrown = true
374+
assert(ex.getMessage.contains("Failed to create resource"))
375+
}
376+
377+
assert(exceptionThrown)
378+
assert(resource1.actionCallCount == 0)
379+
assert(resource1.closeCallCount == 1)
380+
assert(resource2 == null)
381+
}
382+
}
383+
384+
"withFilter (for-comprehension guards)" should {
385+
"skip the body when the guard is false (foreach form) and still close the resource" in {
386+
val res = new AutoCloseableSpy()
387+
388+
var bodyRan = false
389+
for {
390+
r <- res if false
391+
} {
392+
bodyRan = true
393+
r.dummyAction()
394+
}
395+
396+
assert(!bodyRan)
397+
assert(res.actionCallCount == 0)
398+
assert(res.closeCallCount == 1)
399+
}
400+
401+
"run the body when the guard is true (foreach form) and close the resource" in {
402+
val res = new AutoCloseableSpy()
403+
404+
var bodyRan = false
405+
for {
406+
r <- res if true
407+
} {
408+
bodyRan = true
409+
r.dummyAction()
410+
}
411+
412+
assert(bodyRan)
413+
assert(res.actionCallCount == 1)
414+
assert(res.closeCallCount == 1)
415+
}
416+
417+
"close both resources when an inner guard is false (nested generators)" in {
418+
val r1 = new AutoCloseableSpy()
419+
val r2 = new AutoCloseableSpy()
420+
421+
var bodyRan = false
422+
for {
423+
a <- r1
424+
b <- r2 if false
425+
} {
426+
bodyRan = true
427+
a.dummyAction()
428+
b.dummyAction()
429+
}
430+
431+
assert(!bodyRan)
432+
assert(r1.actionCallCount == 0)
433+
assert(r2.actionCallCount == 0)
434+
assert(r2.closeCallCount == 1)
435+
assert(r1.closeCallCount == 1)
436+
}
437+
438+
"compose multiple guards correctly (both must be true)" in {
439+
val res = new AutoCloseableSpy()
440+
441+
var bodyRan = false
442+
for {
443+
r <- res if true if false
444+
} {
445+
bodyRan = true
446+
r.dummyAction()
447+
}
448+
449+
assert(!bodyRan)
450+
assert(res.actionCallCount == 0)
451+
assert(res.closeCallCount == 1)
452+
}
453+
454+
"not evaluate the body when the first guard is false (side-effect check)" in {
455+
val res = new AutoCloseableSpy()
456+
457+
var sideEffect = 0
458+
for {
459+
r <- res if false if { sideEffect += 1; true }
460+
} {
461+
r.dummyAction()
462+
}
463+
464+
assert(sideEffect == 0)
465+
assert(res.actionCallCount == 0)
466+
assert(res.closeCallCount == 1)
467+
}
468+
469+
"throw on yield when the guard is false (map path) and still close the resource" in {
470+
val res = new AutoCloseableSpy()
471+
472+
var thrown = false
473+
try {
474+
val _ = for {
475+
r <- res if false
476+
} yield {
477+
r.dummyAction()
478+
1
479+
}
480+
} catch {
481+
case _: NoSuchElementException => thrown = true
482+
}
483+
484+
assert(thrown)
485+
assert(res.actionCallCount == 0)
486+
assert(res.closeCallCount == 1)
487+
}
488+
489+
"throw on a guarded middle generator in a yield (flatMap->map path) and close all opened resources" in {
490+
val r1 = new AutoCloseableSpy()
491+
val r2 = new AutoCloseableSpy()
492+
493+
var thrown = false
494+
try {
495+
val _ = for {
496+
a <- r1
497+
b <- r2 if false
498+
} yield {
499+
a.dummyAction()
500+
b.dummyAction()
501+
1
502+
}
503+
} catch {
504+
case _: NoSuchElementException => thrown = true
505+
}
506+
507+
assert(thrown)
508+
assert(r1.actionCallCount == 0)
509+
assert(r2.actionCallCount == 0)
510+
assert(r2.closeCallCount == 1)
511+
assert(r1.closeCallCount == 1)
512+
}
269513
}
270514
}

0 commit comments

Comments
 (0)