Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
503 additions
and
421 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
src/main/kotlin/com/danneu/kog/negotiation/AcceptLanguage.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package com.danneu.kog.negotiation | ||
|
||
import kotlin.comparisons.compareBy | ||
|
||
// A **goofy** wrapper around accept-language prefix/suffix pairs like en, en-GB, en-US. | ||
// | ||
// The implementation got a bit gnarly since I was reverse-engineering how it should work from | ||
// other server test cases and letting TDD drive my impl like a black box in some parts. | ||
// | ||
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html | ||
// | ||
// TODO: Incomplete, experimental. | ||
// TODO: Finish language code list http://www.lingoes.net/en/translator/langcode.htm | ||
// TODO: Maybe should keep it open-ended like any language being able to use any locale. | ||
// TODO: Maybe should just simplify it into Pair("en", null), Pair("en", "US") style stuff. | ||
|
||
|
||
// This class is just a pair of language and its q-value. | ||
// | ||
// TODO: Maybe AcceptLanguage should be Pair<Lang, QValue>. Lang vs AcceptLanguage is confusing as top-level classes. | ||
class AcceptLanguage(val lang: Lang, val q: Double = 1.0) { | ||
override fun toString() = "AcceptLanguage[lang=$lang, q=$q]" | ||
override fun equals(other: Any?) = other is AcceptLanguage && this.lang == other.lang && this.q == other.q | ||
|
||
// Generated by IDEA | ||
override fun hashCode() = 31 * lang.hashCode() + q.hashCode() | ||
|
||
companion object { | ||
val regex = Regex("""^\s*([^\s\-;]+)(?:-([^\s;]+))?\s*(?:;(.*))?$""") | ||
|
||
// TODO: Test malformed header | ||
|
||
fun acceptable(clientLang: Lang, availableLang: Lang, excludedLangs: Set<Lang>): Boolean { | ||
if (availableLang in excludedLangs) return false | ||
// clientLang is * so everything is acceptable | ||
if (clientLang == Lang.Wildcard) return true | ||
// if clientLang is "en" and "en-US" is available, it is acceptable | ||
if (clientLang.locale == Locale.Wildcard && clientLang.name == availableLang.name) return true | ||
// if clientLang is "en-US" and "en-US" is available | ||
if (clientLang.name == availableLang.name) return true | ||
|
||
return false | ||
} | ||
|
||
/** Parses a single segment pair | ||
*/ | ||
fun parse(string: String): AcceptLanguage? { | ||
val parts = regex.find(string)?.groupValues?.drop(1) ?: return null | ||
// FIXME: temp hack since the refactor: rejoining parts with hyphen because that's what regex gives me | ||
val lang = Lang.fromString("${parts[0]}-${parts[1]}") ?: return null | ||
val q = QValue.parse(parts[2]) ?: 1.0 | ||
return AcceptLanguage(lang, q) | ||
} | ||
|
||
|
||
/** Parses comma-delimited string of types | ||
*/ | ||
fun parseHeader(header: String): List<AcceptLanguage> { | ||
return header.split(",").map(String::trim).mapNotNull(this::parse) | ||
} | ||
|
||
fun prioritize(xs: List<AcceptLanguage>): List<AcceptLanguage> { | ||
return xs.sortedWith(compareBy( | ||
{ -it.q }, | ||
// Wildcard comes last | ||
{ when (it.lang) { | ||
Lang.Wildcard -> | ||
1 | ||
else -> | ||
0 | ||
}} | ||
)) | ||
|
||
} | ||
} | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package com.danneu.kog.negotiation | ||
|
||
// TODO: Finish implementing http://www.lingoes.net/en/translator/langcode.htm | ||
|
||
sealed class Lang(val name: String, val locale: Locale = Locale.Wildcard) { | ||
companion object { | ||
fun fromString(input: String): Lang? { | ||
val (prefix, localeString) = input.split("-", limit = 2) | ||
|
||
val locale = if (localeString.isEmpty()) { | ||
Locale.Wildcard | ||
} else { | ||
Locale.fromString(localeString) | ||
} | ||
|
||
// TODO: Decide how to handle languages with unknown locales. For now, they are skipped | ||
locale ?: return null | ||
|
||
return when (prefix) { | ||
"*" -> Wildcard | ||
"en" -> English(locale) | ||
"es" -> Spanish(locale) | ||
"de" -> German(locale) | ||
"it" -> Italian(locale) | ||
"fr" -> French(locale) | ||
"pt" -> Portuguese(locale) | ||
"no" -> Norwegian(locale) | ||
"se" -> Sami(locale) | ||
"fi" -> Finnish(locale) | ||
"ro" -> Romanian(locale) | ||
"nl" -> Dutch(locale) | ||
else -> null | ||
} | ||
} | ||
} | ||
|
||
// Necessary for .distinctBy to work | ||
override fun hashCode() = 31 * name.hashCode() + locale.hashCode() | ||
|
||
override fun toString() = if (locale == Locale.Wildcard) { | ||
"$name[*]" | ||
} else { | ||
"$name[$locale]" | ||
} | ||
|
||
override fun equals(other: Any?): Boolean { | ||
return other is Lang && name == other.name && locale == other.locale | ||
} | ||
|
||
object Wildcard : Lang("Wildcard") | ||
class English (locale: Locale = Locale.Wildcard) : Lang("English", locale) | ||
class Spanish (locale: Locale = Locale.Wildcard) : Lang("Spanish", locale) | ||
class German (locale: Locale = Locale.Wildcard) : Lang("German", locale) | ||
class French (locale: Locale = Locale.Wildcard) : Lang("French", locale) | ||
class Italian (locale: Locale = Locale.Wildcard) : Lang("Italian", locale) | ||
class Portuguese (locale: Locale = Locale.Wildcard) : Lang("Portuguese", locale) | ||
class Norwegian (locale: Locale = Locale.Wildcard) : Lang("Norwegian", locale) | ||
class Sami (locale: Locale = Locale.Wildcard) : Lang("Sami", locale) | ||
class Finnish (locale: Locale = Locale.Wildcard) : Lang("Finnish", locale) | ||
class Romanian (locale: Locale = Locale.Wildcard) : Lang("Romanian", locale) | ||
class Dutch (locale: Locale = Locale.Wildcard) : Lang("Dutch", locale) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package com.danneu.kog.negotiation | ||
|
||
import com.danneu.kog.Header | ||
import com.danneu.kog.Request | ||
import com.danneu.kog.toy | ||
|
||
enum class Locale { | ||
// Special | ||
Wildcard, | ||
|
||
// Locales | ||
Germany, | ||
Argentina, | ||
Australia, | ||
Belize, | ||
Brazil, | ||
Liechtenstein, | ||
Luxembourg, | ||
Switzerland, | ||
Bolivia, | ||
Canada, | ||
Caribbean, | ||
Chile, | ||
Colombia, | ||
CostaRica, | ||
DominicanRepublic, | ||
Ecuador, | ||
ElSalvador, | ||
Finland, | ||
Guatemala, | ||
Honduras, | ||
Ireland, | ||
Romania, | ||
Jamaica, | ||
Mexico, | ||
NewZealand, | ||
Nicaragua, | ||
Panama, | ||
Paraguay, | ||
Peru, | ||
Philippines, | ||
Portugal, | ||
PuertoRico, | ||
SouthAfrica, | ||
Spain, | ||
Austria, | ||
TrinidadAndTobago, | ||
UnitedKingdom, | ||
UnitedStates, | ||
Uruguay, | ||
Venezuela, | ||
Zimbabwe | ||
; | ||
|
||
companion object { | ||
fun fromString(input: String): Locale? = when (input.toUpperCase()) { | ||
"AR" -> Argentina | ||
"LU" -> Luxembourg | ||
"AU" -> Australia | ||
"BO" -> Bolivia | ||
"BR" -> Brazil | ||
"RO" -> Romania | ||
"BZ" -> Belize | ||
"CA" -> Canada | ||
"CB" -> Caribbean | ||
"CL" -> Chile | ||
"CO" -> Colombia | ||
"CR" -> CostaRica | ||
"DO" -> DominicanRepublic | ||
"EC" -> Ecuador | ||
"ES" -> Spain | ||
"GB" -> UnitedKingdom | ||
"GT" -> Guatemala | ||
"FI" -> Finland | ||
"HN" -> Honduras | ||
"IE" -> Ireland | ||
"JM" -> Jamaica | ||
"MX" -> Mexico | ||
"NI" -> Nicaragua | ||
"NZ" -> NewZealand | ||
"PA" -> Panama | ||
"PE" -> Peru | ||
"PH" -> Philippines | ||
"PR" -> PuertoRico | ||
"PT" -> Portugal | ||
"PY" -> Paraguay | ||
"SV" -> ElSalvador | ||
"TT" -> TrinidadAndTobago | ||
"US" -> UnitedStates | ||
"DE" -> Germany | ||
"LI" -> Liechtenstein | ||
"UY" -> Uruguay | ||
"VE" -> Venezuela | ||
"ZA" -> SouthAfrica | ||
"ZW" -> Zimbabwe | ||
"AT" -> Austria | ||
"CH" -> Switzerland | ||
else -> null | ||
} | ||
} | ||
} | ||
|
||
enum class AvailableLang { | ||
Spanish, | ||
English | ||
} | ||
|
||
fun Request.lang(): AvailableLang { | ||
val availableLangs = listOf( | ||
Lang.Spanish(), | ||
Lang.English() | ||
) | ||
|
||
return when (this.negotiate.acceptableLanguage(availableLangs)) { | ||
Lang.English() -> AvailableLang.English | ||
else -> AvailableLang.Spanish | ||
} | ||
} | ||
|
||
fun main(args: Array<String>) { | ||
|
||
//val neg = Negotiator(Request.toy(headers = mutableListOf(Header.AcceptLanguage to "en-US, es"))) | ||
val req = Request.toy(headers = mutableListOf(Header.AcceptLanguage to "es, en-US, es")) | ||
|
||
println(req.lang()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.