@@ -51,7 +51,7 @@ object Main extends App with DependencyInjectionModule with LazyLogging { | |||
bindingFuture.onComplete { | |||
case Success(_) => | |||
println(s"Server for started at localhost:8080") | |||
println(s"Server for started at localhost:9080") | |||
case Failure(cause) => | |||
println(s"Server failed to start", cause) | |||
} |
@@ -4,7 +4,7 @@ import akka.actor.ActorSystem | |||
import akka.http.scaladsl.model._ | |||
import akka.http.scaladsl.server.Directives.{pathPrefix, _} | |||
import akka.http.scaladsl.server._ | |||
import akka.stream.ActorMaterializer | |||
import akka.stream.{ActorMaterializer, Materializer} | |||
import akka.util.ByteString | |||
import de.codingchallenge.configuration.Environment | |||
import com.typesafe.scalalogging.LazyLogging | |||
@@ -16,8 +16,8 @@ class Routes(articleExportService: ArticleExportService, | |||
environment: Environment) | |||
extends LazyLogging { | |||
implicit val ec = actorSystem | |||
implicit val amaterializer = actorMaterializer | |||
implicit val as: ActorSystem = actorSystem | |||
implicit val mat: Materializer = actorMaterializer | |||
var on: Boolean = true | |||
@@ -44,8 +44,8 @@ class Routes(articleExportService: ArticleExportService, | |||
val exportRoute: Route = | |||
path("articles") { | |||
get { | |||
onSuccess(articleExportService.exportArticles()) { _ => | |||
complete(StatusCodes.NoContent) | |||
onSuccess(articleExportService.exportArticles()) { res => | |||
complete(res) | |||
} | |||
} | |||
} |
@@ -1,13 +1,27 @@ | |||
package de.codingchallenge.models | |||
import java.text.DecimalFormat | |||
import java.util.Locale | |||
import de.codingchallenge.csv.CsvColumnWrites | |||
/** | |||
* Data structure representing the CSV export | |||
* @param productId product group identifier | |||
* @param name name of the product | |||
* @param description description of the product | |||
* @param price price | |||
* @param stockSum stock over group | |||
*/ | |||
case class ProductExport(productId: String, name: String, description: String, price:Float, stockSum: Int) | |||
object ProductExport{ | |||
object ProductExport { | |||
implicit val columnWrites: CsvColumnWrites[ProductExport] = (p: ProductExport) => Seq( | |||
p.productId, p.name, p.description,"%.2f".formatLocal(java.util.Locale.US, p.price), p.stockSum.toString | |||
) | |||
implicit val columnWrites: CsvColumnWrites[ProductExport] = (p: ProductExport) => | |||
p.productIterator.map(_.toString).toSeq | |||
def apply(a: Article, stockSum: Int): ProductExport = new ProductExport( | |||
productId = a.productId, |
@@ -3,9 +3,9 @@ package de.codingchallenge.repositories | |||
import akka.NotUsed | |||
import akka.actor.ActorSystem | |||
import akka.http.scaladsl.Http | |||
import akka.http.scaladsl.model.HttpEntity.Chunked | |||
import akka.http.scaladsl.model._ | |||
import akka.stream.scaladsl.{Flow, Source} | |||
import akka.stream.{ActorMaterializer, Materializer} | |||
import akka.util.ByteString | |||
import com.typesafe.scalalogging.LazyLogging | |||
import de.codingchallenge.csv.CsvOps._ | |||
@@ -16,6 +16,7 @@ import scala.concurrent.{ExecutionContext, Future} | |||
class ProductExportRepository(actorSystem: ActorSystem)(implicit ec: ExecutionContext) extends LazyLogging { | |||
private implicit val as: ActorSystem = actorSystem | |||
private implicit val mat: Materializer = ActorMaterializer() | |||
val headerLine = "produktId|name|beschreibung|preis|summeBestand" | |||
@@ -28,27 +29,25 @@ class ProductExportRepository(actorSystem: ActorSystem)(implicit ec: ExecutionCo | |||
.intersperse("\n") | |||
.map(ByteString.apply) | |||
// it did not work with charset information | |||
val entity = Chunked.fromData( | |||
ContentType.WithMissingCharset(MediaTypes.`text/csv`), | |||
sourceWithHeader | |||
) | |||
/** | |||
* Setting charset to utf8 results in HTTP.406 | |||
*/ | |||
Http() | |||
.singleRequest( | |||
HttpRequest( | |||
method = HttpMethods.PUT, | |||
uri = s"$baseUrl/products/$articlesSize", | |||
entity = entity)) | |||
.map{res => | |||
entity = HttpEntity(ContentType.WithMissingCharset(MediaTypes.`text/csv`), sourceWithHeader) | |||
)) | |||
.map { res => | |||
logger.info(s"Server responded with $res") | |||
res | |||
} | |||
} | |||
private val csvFlow: Flow[ProductExport, String, NotUsed] = | |||
Flow.fromFunction{p => | |||
logger.info(s"processing export record $p") | |||
Flow.fromFunction { p => | |||
logger.info(s"streaming export record $p") | |||
p.toCsvLine | |||
} | |||
@@ -18,7 +18,7 @@ class ArticleExportService( | |||
) extends LazyLogging { | |||
implicit val m: Materializer = mat | |||
val productsSize: Int = 100 | |||
val productsSize: Int = 20 | |||
/** | |||
* Streams articles to generate a product export. | |||
@@ -34,16 +34,20 @@ class ArticleExportService( | |||
def exportArticles(): Future[HttpResponse] = productExportRepository.add(Source.fromGraph[ProductExport, NotUsed]( | |||
articleRepository | |||
.getArticles(productsSize) | |||
.filter(_.stock > 0) | |||
// .filter(_.stock > 0) | |||
.map(a => (a, a.stock)) | |||
.via(new AccumulateWhileUnchanged[(Article, Int), String](_._1.productId)) | |||
.map(_.reduce[(Article, Int)] { | |||
case ((a1, c1), (a2, c2)) if a1.price > a2.price => (a2, c1 + c2) | |||
case ((a1, c1), (_, c2)) => (a1, c1 + c2) | |||
}) | |||
.map(reduceTuples) | |||
.map { case (article, stockSum) => | |||
logger.info(s"Reduced to article: $article and stockSum: $stockSum") | |||
ProductExport(article, stockSum) } | |||
), productsSize) | |||
def reduceTuples(t: Seq[(Article, Int)]): (Article, Int) = { | |||
logger.info(s"reducing group $t") | |||
t.reduceLeft[(Article, Int)] { | |||
case ((a1, c1), (a2, c2)) if a1.price > a2.price => (a2, c1 + c2) | |||
case ((a1, c1), (_, c2)) => (a1, c1 + c2) | |||
} | |||
} | |||
} |
@@ -123,4 +123,14 @@ trait ArticleFixture { | |||
A-BdclQqXJ|P-BdclQqXJ|J YSQPS||22.28|49 | |||
A-OHAeNBak|P-BdclQqXJ|JWWJVYSQPS C TRYDYX|pvAlqsw bnln ijxmdog roqumthtp lijqubnzz|97.32|24 | |||
A-dclQqXJ7|P-BdclQqXJ|QPSU||77.34|17""".split("\n").map(_.trim) | |||
val sampleGroup = Seq( | |||
"A-C7PeyUjD|P-C7PeyUjD|MGT|swwfkg rzDawtwl vvvjebHd aenb aXkcwJyqsc|77.34|17", | |||
"A-LPeyUjDg|P-C7PeyUjD|BMSNWL HFH||45.85|0", | |||
"A-7PeyUjDg|P-C7PeyUjD|BMSNWL HFH||45.85|0", | |||
"A-peyUjDgF|P-C7PeyUjD|SN L|hkqrz u wtwllbkxkl Hdwcpl|87.72|1", | |||
"A-DYMxSQVE|P-C7PeyUjD|GTFB|wwfkgu zDawtw|84.12|7", | |||
"A-PeyUjDgF|P-C7PeyUjD|SN L|hkqrz u wtwllbkxkl Hdwcpl|87.72|1" | |||
) | |||
} |
@@ -49,7 +49,7 @@ class ArticleExportServiceSpec extends BaseSpec with AkkaSpec{ | |||
verify(productExportRepositoryMock).add(sourceCaptor.capture(), any()) | |||
Await.result(sourceCaptor.getValue.runWith(Sink.head), 1.second) mustBe productExport | |||
} | |||
"pass sample data as expected" in new TestSetup with ArticleFixture { | |||
"contain only one entry per product" in new TestSetup with ArticleFixture { | |||
val articles = sampleData.map(_.csvToOptOf[Article].get).toList | |||
doReturn(Source(articles), Nil: _*) | |||
@@ -61,9 +61,31 @@ class ArticleExportServiceSpec extends BaseSpec with AkkaSpec{ | |||
Await.result(service.exportArticles(), 5.second) | |||
val sourceCaptor: ArgumentCaptor[Source[ProductExport, NotUsed]] = ArgumentCaptor.forClass(classOf[Source[ProductExport, NotUsed]]) | |||
verify(productExportRepositoryMock).add(sourceCaptor.capture(), any()) | |||
Await.result(sourceCaptor.getValue.runWith(Sink.seq), 1.second).foreach { e => | |||
e mustBe a[ProductExport] | |||
} | |||
val results = Await.result(sourceCaptor.getValue.runWith(Sink.seq), 1.second) | |||
results.foreach{ | |||
r => results.count(_.productId == r.productId) == 1 | |||
} | |||
} | |||
"find the cheapest element of a group" in new TestSetup with ArticleFixture { | |||
val articles = sampleGroup.map(_.csvToOptOf[Article].get).toList | |||
doReturn(Source(articles), Nil: _*) | |||
.when(articleRepositoryMock) | |||
.getArticles(100) | |||
when(productExportRepositoryMock.add(any(), any())) | |||
.thenReturn(Future.successful(HttpResponse())) | |||
Await.result(service.exportArticles(), 5.second) | |||
val sourceCaptor: ArgumentCaptor[Source[ProductExport, NotUsed]] = ArgumentCaptor.forClass(classOf[Source[ProductExport, NotUsed]]) | |||
verify(productExportRepositoryMock).add(sourceCaptor.capture(), any()) | |||
val results = Await.result(sourceCaptor.getValue.runWith(Sink.seq), 1.second) | |||
results.head mustBe ProductExport( | |||
productId = "P-C7PeyUjD", | |||
name = "BMSNWL HFH", | |||
description = "", | |||
price = 45.85f, | |||
stockSum = 26 | |||
) | |||
} | |||
} | |||
} |