diff --git a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala index de73fb91ec..eacabc5f86 100644 --- a/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala +++ b/akka-docs-dev/rst/scala/code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala @@ -58,6 +58,25 @@ class MiscDirectivesExamplesSpec extends RoutingSpec { responseAs[String] shouldEqual "request entity empty" } } + "selectPreferredLanguage-example" in { + val request = Get() ~> `Accept-Language`( + Language("en-US"), + Language("en") withQValue 0.7f, + LanguageRange.`*` withQValue 0.1f, + Language("de") withQValue 0.5f) + + request ~> { + selectPreferredLanguage("en", "en-US") { lang ⇒ + complete(lang.toString) + } + } ~> check { responseAs[String] shouldEqual "en-US" } + + request ~> { + selectPreferredLanguage("de-DE", "hu") { lang ⇒ + complete(lang.toString) + } + } ~> check { responseAs[String] shouldEqual "de-DE" } + } "validate-example" in { val route = extractUri { uri => diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/alphabetically.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/alphabetically.rst index 276f7dc681..86a93aa8c4 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/directives/alphabetically.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/alphabetically.rst @@ -197,6 +197,8 @@ Directive Description :ref:`-responseEncodingAccepted-` Rejects the request with an ``UnacceptedResponseEncodingRejection`` if the given response encoding is not accepted by the client :ref:`-scheme-` Rejects all requests whose URI scheme doesn't match the given one +:ref:`-selectPreferredLanguage-` Inspects the request's ``Accept-Language`` header and determines, which of + a given set of language alternatives is preferred by the client :ref:`-setCookie-` Adds a ``Set-Cookie`` response header with the given cookies :ref:`-textract-` Extracts a number of values using a ``RequestContext ⇒ Tuple`` function :ref:`-tprovide-` Injects a given tuple of values into a directive diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/index.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/index.rst index ecbd69f162..73837a2350 100644 --- a/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/index.rst +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/index.rst @@ -10,4 +10,5 @@ MiscDirectives rejectEmptyResponse requestEntityEmpty requestEntityPresent + selectPreferredLanguage validate \ No newline at end of file diff --git a/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/selectPreferredLanguage.rst b/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/selectPreferredLanguage.rst new file mode 100644 index 0000000000..5b5b6647c1 --- /dev/null +++ b/akka-docs-dev/rst/scala/http/routing-dsl/directives/misc-directives/selectPreferredLanguage.rst @@ -0,0 +1,25 @@ +.. _-selectPreferredLanguage-: + +selectPreferredLanguage +======================= + +Signature +--------- + +.. includecode2:: /../../akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala + :snippet: selectPreferredLanguage + +Description +----------- +Inspects the request's ``Accept-Language`` header and determines, +which of a given set of language alternatives is preferred by the client according to content negotiation rules +defined by http://tools.ietf.org/html/rfc7231#section-5.3.5. + +If there are several best language alternatives that the client has equal preference for +(even if this preference is zero!) the order of the arguments is used as a tie breaker (first one wins). + +Example +------- + +.. includecode2:: ../../../../code/docs/http/scaladsl/server/directives/MiscDirectivesExamplesSpec.scala + :snippet: selectPreferredLanguage-example diff --git a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala index 3c4b066e40..deacba2401 100644 --- a/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala +++ b/akka-http-tests/src/test/scala/akka/http/scaladsl/server/directives/MiscDirectivesSpec.scala @@ -5,11 +5,11 @@ package akka.http.scaladsl.server package directives +import scala.concurrent.{ Await, Promise } +import scala.concurrent.duration._ +import scala.util.Try import akka.http.scaladsl.model._ import headers._ -import HttpMethods._ -import MediaTypes._ -import Uri._ class MiscDirectivesSpec extends RoutingSpec { @@ -30,4 +30,63 @@ class MiscDirectivesSpec extends RoutingSpec { } ~> check { responseAs[String] shouldEqual "1.2.3.4" } } } + + "the selectPreferredLanguage directive" should { + "Accept-Language: de, en" test { selectFrom ⇒ + selectFrom("de", "en") shouldEqual "de" + selectFrom("en", "de") shouldEqual "de" + } + "Accept-Language: en, de" test { selectFrom ⇒ + selectFrom("de", "en") shouldEqual "en" + selectFrom("en", "de") shouldEqual "en" + } + "Accept-Language: en, de;q=.5" test { selectFrom ⇒ + selectFrom("de", "en") shouldEqual "en" + selectFrom("en", "de") shouldEqual "en" + } + "Accept-Language: en;q=.5, de" test { selectFrom ⇒ + selectFrom("de", "en") shouldEqual "de" + selectFrom("en", "de") shouldEqual "de" + } + "Accept-Language: en-US, en;q=.7, *;q=.1, de;q=.5" test { selectFrom ⇒ + selectFrom("en", "en-US") shouldEqual "en-US" + selectFrom("de", "en") shouldEqual "en" + selectFrom("de", "hu") shouldEqual "de" + selectFrom("de-DE", "hu") shouldEqual "de-DE" + selectFrom("hu", "es") shouldEqual "hu" + selectFrom("es", "hu") shouldEqual "es" + } + "Accept-Language: en, *;q=.5, de;q=0" test { selectFrom ⇒ + selectFrom("es", "de") shouldEqual "es" + selectFrom("de", "es") shouldEqual "es" + selectFrom("es", "en") shouldEqual "en" + } + "Accept-Language: en, *;q=0" test { selectFrom ⇒ + selectFrom("es", "de") shouldEqual "es" + selectFrom("de", "es") shouldEqual "de" + selectFrom("es", "en") shouldEqual "en" + } + } + + implicit class AddStringToIn(acceptLanguageHeaderString: String) { + def test(body: ((String*) ⇒ String) ⇒ Unit): Unit = + s"properly handle `$acceptLanguageHeaderString`" in { + val Array(name, value) = acceptLanguageHeaderString.split(':') + val acceptLanguageHeader = HttpHeader.parse(name.trim, value) match { + case HttpHeader.ParsingResult.Ok(h: `Accept-Language`, Nil) ⇒ h + case result ⇒ fail(result.toString) + } + body { availableLangs ⇒ + val selected = Promise[String]() + val first = Language(availableLangs.head) + val more = availableLangs.tail.map(Language(_)) + Get() ~> addHeader(acceptLanguageHeader) ~> { + selectPreferredLanguage(first, more: _*) { lang ⇒ + complete(lang.toString) + } + } ~> check(selected.complete(Try(responseAs[String]))) + Await.result(selected.future, 1.second) + } + } + } } diff --git a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala index bfdd1ef7e7..dec77516e3 100644 --- a/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala +++ b/akka-http/src/main/scala/akka/http/scaladsl/server/directives/MiscDirectives.scala @@ -41,6 +41,29 @@ trait MiscDirectives { * be treated as if the request could not be matched. */ def rejectEmptyResponse: Directive0 = MiscDirectives._rejectEmptyResponse + + /** + * Inspects the request's `Accept-Language` header and determines, + * which of the given language alternatives is preferred by the client. + * (See http://tools.ietf.org/html/rfc7231#section-5.3.5 for more details on the + * negotiation logic.) + * If there are several best language alternatives that the client + * has equal preference for (even if this preference is zero!) + * the order of the arguments is used as a tie breaker (First one wins). + */ + def selectPreferredLanguage(first: Language, more: Language*): Directive1[Language] = { + val available = first :: List(more: _*) // we use List rather than Seq since element count is likely very small + BasicDirectives.extractRequest.map { req ⇒ + val sortedWithQValues = available.zip(available.map(req.qValueForLanguage(_))).sortBy(-_._2) + val firstBest = sortedWithQValues.head + val moreBest = sortedWithQValues.tail.takeWhile(_._2 == firstBest._2) + if (moreBest.nonEmpty) { + // we have several languages that have the same qvalue, so we pick the one the `Accept-Header` lists first + val allBest = firstBest :: moreBest + req.acceptedLanguageRanges.flatMap(range ⇒ allBest.find(t ⇒ range.matches(t._1))).head._1 + } else firstBest._1 // we have a single best match, so pick that + } + } } object MiscDirectives extends MiscDirectives {