Skip to content

Commit 9c9ee97

Browse files
committed
Support support-fetching-then-parsing-with-key
# Conflicts: # core/src/test/scala/com/gu/etagcaching/LoadingTest.scala
1 parent d4fd0a4 commit 9c9ee97

File tree

4 files changed

+59
-15
lines changed

4 files changed

+59
-15
lines changed

core/src/main/scala/com/gu/etagcaching/Loading.scala

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import scala.concurrent.{ExecutionContext, Future}
88
/**
99
* `Loading` represents the two sequential steps of getting something useful from a remote resource, specifically:
1010
*
11-
* - Fetching
11+
* - [[Fetching]]
1212
* - Parsing
1313
*
14-
* Our [[Fetching]] interface requires supporting conditional-fetching based on `ETag`s, which means that
15-
* we can short-cut the Parsing step if the resource hasn't changed.
14+
* The [[Fetching]] interface supports conditional-fetching based on `ETag`s, allowing us to short-cut the Parsing step
15+
* if the resource hasn't changed.
16+
*
17+
* The easiest way to get a [[Loading]] instance is with a [[Fetching]] instance - just call
18+
* [[Fetching.thenParsing]] to create a [[Loading]] instance.
1619
*
1720
* @tparam K The 'key' or resource identifier type - for instance, a URL or S3 Object Id.
1821
* @tparam V The 'value' for the key - a parsed representation of whatever was in the resource data.
@@ -34,17 +37,6 @@ trait Loading[K, V] {
3437
}
3538

3639
object Loading {
37-
def by[K, Response, V](fetching: Fetching[K, Response])(parse: Response => V)(implicit parsingEC: ExecutionContext): Loading[K, V] = new Loading[K, V] {
38-
def fetchAndParse(key: K): Future[MissingOrETagged[V]] =
39-
fetching.fetch(key).map(_.map(parse))
40-
41-
def fetchThenParseIfNecessary(key: K, oldV: ETaggedData[V]): Future[MissingOrETagged[V]] =
42-
fetching.fetchOnlyIfETagChanged(key, oldV.eTag).map {
43-
case None => oldV // we got HTTP 304 'NOT MODIFIED': there's no new data - old data is still valid
44-
case Some(freshResponse) => freshResponse.map(parse)
45-
}
46-
}
47-
4840
/**
4941
* Represents an update event for a given key.
5042
*/

core/src/main/scala/com/gu/etagcaching/fetching/Fetching.scala

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,31 @@ trait Fetching[K, Response] {
3737

3838
def mapResponse[Response2](f: Response => Response2)(implicit ec: ExecutionContext): Fetching[K, Response2] = ResponseMapper(this)(f)
3939

40-
def thenParsing[V](parse: Response => V)(implicit parsingEC: ExecutionContext): Loading[K, V] = Loading.by(this)(parse)
40+
/**
41+
* Transforms this [[Fetching]] instance into a full [[Loading]] instance by saying how to parse
42+
* the fetched response.
43+
*
44+
* If you happen to need the `key` to parse the response, use [[thenParsingWithKey]].
45+
*/
46+
def thenParsing[V](parse: Response => V)(implicit parsingEC: ExecutionContext): Loading[K, V] = thenParsingWithKey((_, response) => parse(response))
47+
48+
/**
49+
* Transforms this [[Fetching]] instance into a full [[Loading]] instance by saying how to parse
50+
* the fetched response - in this particular case, the parsing method is passed the `key`
51+
* as well as the fetched response.
52+
*
53+
* If you don't need the `key` to parse the response, just use [[thenParsing]].
54+
*/
55+
def thenParsingWithKey[V](parse: (K, Response) => V)(implicit parsingEC: ExecutionContext): Loading[K, V] = new Loading[K, V] {
56+
def fetchAndParse(key: K): Future[MissingOrETagged[V]] =
57+
fetch(key).map(_.map(parse(key, _)))
58+
59+
def fetchThenParseIfNecessary(key: K, oldV: ETaggedData[V]): Future[MissingOrETagged[V]] =
60+
fetchOnlyIfETagChanged(key, oldV.eTag).map {
61+
case None => oldV // we got HTTP 304 'NOT MODIFIED': there's no new data - old data is still valid
62+
case Some(freshResponse) => freshResponse.map(parse(key, _))
63+
}
64+
}
4165
}
4266

4367
object Fetching {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.gu.etagcaching
2+
3+
import java.util.concurrent.atomic.AtomicLong
4+
5+
class CountingParser[K,V](parser: K => V) extends (K => V) {
6+
7+
private val counter = new AtomicLong()
8+
9+
override def apply(key: K): V = {
10+
counter.incrementAndGet()
11+
parser(key)
12+
}
13+
14+
def count(): Long = counter.get()
15+
}

core/src/test/scala/com/gu/etagcaching/LoadingTest.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import org.scalatest.{Inside, OptionValues}
1515

1616
import java.time.DayOfWeek
1717
import java.time.DayOfWeek.{MONDAY, SATURDAY, THURSDAY}
18+
import java.time.format.DateTimeFormatter
19+
import java.time.temporal.ChronoField.DAY_OF_WEEK
1820
import java.util.Locale
1921
import java.util.Locale.{FRANCE, GERMANY, UK}
2022
import scala.collection.mutable
@@ -51,6 +53,17 @@ class LoadingTest extends AnyFlatSpec with Matchers with ScalaFutures with Optio
5153
loading.fetchAndParse(GERMANY).futureValue.toOption.value shouldBe MONDAY
5254
}
5355

56+
it should "be possible with 'thenParsingWithKey', if we need the key for the 'parsing' phase" in {
57+
val fetching: Fetching[Locale, String] =
58+
TestFetching.withStubDataStore(Map(FRANCE -> "jeudi", GERMANY -> "Montag"))
59+
60+
val loading: Loading[Locale, DayOfWeek] = fetching.thenParsingWithKey((localeKey, response) =>
61+
DayOfWeek.of(DateTimeFormatter.ofPattern("EEEE", localeKey).parse(response).get(DAY_OF_WEEK)))
62+
63+
loading.fetchAndParse(FRANCE).futureValue.toOption.value shouldBe THURSDAY
64+
loading.fetchAndParse(GERMANY).futureValue.toOption.value shouldBe MONDAY
65+
}
66+
5467
"fetchThenParseIfNecessary" should "*only* do parsing if fetching found a change in ETag value" in {
5568
val dataStore = mutable.Map(UK -> "SATURDAY")
5669
val testApparatus = new TestApparatus(dataStore)(parser = DayOfWeek.valueOf)

0 commit comments

Comments
 (0)