-
Notifications
You must be signed in to change notification settings - Fork 19
Enhancement #3
base: master
Are you sure you want to change the base?
Enhancement #3
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.epl.akka_v1 | ||
|
||
object Cloudant { | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package com.epl.akka_v1 | ||
|
||
import akka.actor.ActorSystem | ||
import akka.http.scaladsl.Http | ||
import akka.http.scaladsl.marshalling.Marshal | ||
import akka.http.scaladsl.model.headers.BasicHttpCredentials | ||
import akka.http.scaladsl.model.{HttpMethods, HttpRequest, RequestEntity, headers} | ||
import akka.http.scaladsl.unmarshalling.Unmarshal | ||
import akka.stream.ActorMaterializer | ||
import akka.util.ByteString | ||
import com.epl.akka.DocumentType | ||
|
||
import scala.concurrent.Future | ||
|
||
object HttpClient { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd call it a "CloudantClient" or possibly a "CloudantHttpClient" instead, as it isn't a generic HTTP client, but rather provides cloudant specific things on top of the http client. |
||
|
||
implicit val system = ActorSystem() | ||
implicit val materializer = ActorMaterializer() | ||
implicit val ec = system.dispatcher | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't create a separate actor system for this, instead, make this a class that takes the system and materializer as constructor parameters, and pass in the system of the actor where you use it. |
||
|
||
final val config = system.settings.config | ||
|
||
private val authorization = headers.Authorization(BasicHttpCredentials(config.getString("cloudant.username"), config.getString("cloudant.password"))) | ||
|
||
|
||
/** | ||
* POST call to Cloudant database API | ||
* @param jsonString | ||
* @return | ||
*/ | ||
def post(jsonString: String): Future[String] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That the cloudant client does a post/get is internal details which the calling code does not have anything to do with, I think you should raise the abstraction level one step and call these I still think that accepting strings and producing strings is the wrong abstraction, but one step at a time I guess. If it ends up with wanting to avoid parsing the JSON (which I think would be the right thing) for performance reasons I would still introduce a new class denoting that it is not any string but raw json, in that case it may make sense to never even parse it into a string but keep it as bytes. Something like |
||
val responseEntity = for { | ||
request <- Marshal(jsonString).to[RequestEntity] | ||
response <- Http().singleRequest(HttpRequest( | ||
method = HttpMethods.POST, | ||
uri = config.getString("cloudant.post_url"), | ||
headers = List(authorization), | ||
entity = request)) | ||
entity <- Unmarshal(response.entity).to[ByteString] | ||
} yield entity | ||
|
||
responseEntity.mapTo[String] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't look right to me, you cannot just typecast a ByteString to a String, you should either Unmarshal a string above if possible, or you would have to |
||
|
||
} | ||
|
||
|
||
/** | ||
* GET call to Cloudant database API | ||
* @param documentType | ||
* @return | ||
*/ | ||
def get(documentType: DocumentType.Documenttype): Future[String] = { | ||
val responseEntity = for { | ||
response <- Http().singleRequest(HttpRequest( | ||
method = HttpMethods.GET, | ||
uri = getUrl(documentType), | ||
headers = List(authorization))) | ||
entity <- Unmarshal(response.entity).to[ByteString] | ||
} yield entity | ||
|
||
responseEntity.mapTo[String] | ||
|
||
} | ||
|
||
|
||
/** | ||
* Get URI based on the type of document requested. | ||
* @param documentType | ||
* @return | ||
*/ | ||
def getUrl(documentType: DocumentType.Documenttype): String ={ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an internal method, not a part of the public API of this class, so make it private (or package private if you want to be able to write unit tests for it) |
||
var url: String = null | ||
if(documentType.equals(DocumentType.TeamTable)){ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perfect use case for Scala pattern matching, would make this
additionally changing
would then give you a compiler error if you at some future point added another document type but forgot to update the pattern match expression to cover all possible values. |
||
url = config.getString("cloudant.get_tables_url") | ||
}else if(documentType.equals(DocumentType.Results)){ | ||
url = config.getString("cloudant.get_results_url") | ||
}else{ | ||
url = config.getString("cloudant.get_fixtures_url") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Reading config is somewhat costly, so it makes sense to rather read these once and cache the values in fields of the class when constructing the instance, just like you do with the auth header. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would creating a immutable map like below would work?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, you could use a map, but I think just using fields are more clear and allow you to keep separate types with the values rather than either forcing all configurable values to be strings or Note that you should not load the configuration yourself, instead you should use it from the actor context or system, so: class AppConfig(implicit system: ActorSystem, materializer: ActorMaterializer ) {
// gives us a config just containing the things inside cloudant { ... } in the config file
private def config = system.settings.config.getConfig("cloudant")
// these are strings, as for example
private val username = config.getString("username")
private val resultsUrl = config.getString("get_results_url")
// just an example, a boolean setting
private val useHttps = config.getBoolean("use-https")
... the rest of the class ... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ups, of course not private then if you are creating a class that other classes use to know the settings :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inside of Akka (and the surrounding projects) we call such classes object CloudantSettings {
def apply(config: Config) =
CloudAntSettings(config.getString("username"), ... ... )
def apply(system: ActorSystem) =
apply(system.settings.config.getConfig("cloudant"))
}
case class CloudantSettings(
username: String,
password: String,
resultsUrl: String,
...etc...
) This makes it easy to create custom instances both programmatically and from config. |
||
} | ||
url | ||
} | ||
|
||
|
||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like it can be deleted now