API in progress: tournament and registration ok

This commit is contained in:
Claude Brisson
2023-05-15 10:50:51 +02:00
parent c82f36c776
commit 30a9bad259
15 changed files with 248 additions and 191 deletions

18
test.sh
View File

@@ -3,25 +3,17 @@
curl -s --header "Accept: application/json" --header "Content-Type: application/json" \ curl -s --header "Accept: application/json" --header "Content-Type: application/json" \
--request POST \ --request POST \
--data '{ "type":"INDIVIDUAL","name":"Mon Tournoi", "shortName": "mon-tournoi", "startDate": "2023-05-10", "endDate": "2023-05-12", "country": "FR", "location": "Marseille", "online": false, "timeSystem": { "type": "fisher", "mainTime": "1200", "increment": "10" }, "pairing": { "type": "ROUNDROBIN" } }' \ --data '{ "type":"INDIVIDUAL","name":"Mon Tournoi", "shortName": "mon-tournoi", "startDate": "2023-05-10", "endDate": "2023-05-12", "country": "FR", "location": "Marseille", "online": false, "timeSystem": { "type": "fisher", "mainTime": "1200", "increment": "10" }, "pairing": { "type": "ROUNDROBIN" } }' \
http://localhost:8080/api/tournament http://localhost:8080/api/tour
curl -s --header "Accept: application/json" http://localhost:8080/api/tournament curl -s --header "Accept: application/json" http://localhost:8080/api/tour
curl -s --header "Accept: application/json" http://localhost:8080/api/tournament/1 curl -s --header "Accept: application/json" http://localhost:8080/api/tour/1
curl -s --header "Accept: application/json" --header "Content-Type: application/json" \ curl -s --header "Accept: application/json" --header "Content-Type: application/json" \
--request POST \ --request POST \
--data '{ "name": "Burma", "firstname": "Nestor", "rating": 1600, "rank": -2, "country": "FR", "club": "13Ma" }' \ --data '{ "name": "Burma", "firstname": "Nestor", "rating": 1600, "rank": -2, "country": "FR", "club": "13Ma" }' \
http://localhost:8080/api/player http://localhost:8080/api/tour/1/part
curl -s --header "Accept: application/json" http://localhost:8080/api/player
curl -s --header "Accept: application/json" --header "Content-Type: application/json" \
--request POST \
--data '{ "id": 1 }' \
http://localhost:8080/api/tournament/1/registration
curl -s --header "Accept: application/json" http://localhost:8080/api/tournament/1/registration
curl -s --header "Accept: application/json" http://localhost:8080/api/tour/1/part

View File

@@ -0,0 +1,11 @@
package org.jeudego.pairgoth.api
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest
interface PairgothApiHandler: ApiHandler {
fun getTournament(request: HttpServletRequest): Tournament? = getSelector(request)?.toIntOrNull()?.let { Store.getTournament(it) }
}

View File

@@ -0,0 +1,21 @@
package org.jeudego.pairgoth.api
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest
object PairingHandler: ApiHandler {
private fun getTournament(request: HttpServletRequest): Tournament {
val tournamentId = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("invalid tournament id")
return Store.getTournament(tournamentId) ?: ApiHandler.badRequest("unknown tournament id")
}
override fun get(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val round = request.getParameter("round")?.toIntOrNull() ?: badRequest("invalid round number")
return Json.Object();
}
}

View File

@@ -1,35 +1,43 @@
package org.jeudego.pairgoth.api package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Player import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
object PlayerHandler: ApiHandler { object PlayerHandler: PairgothApiHandler {
override fun post(request: HttpServletRequest): Json {
val payload = getObjectPayload(request)
// player parsing
val player = Player.fromJson(payload)
Store.addPlayer(player)
return Json.Object("success" to true, "id" to player.id)
}
override fun get(request: HttpServletRequest): Json { override fun get(request: HttpServletRequest): Json {
return when (val id = getSelector(request)?.toIntOrNull()) { val tournament = getTournament(request) ?: badRequest("invalid tournament")
null -> Json.Array(Store.getPlayersIDs()) return when (val pid = getSubSelector(request)?.toIntOrNull()) {
else -> Store.getPlayer(id)?.toJson() ?: ApiHandler.badRequest("no player with id #${id}") null -> Json.Array(tournament.pairables.values.map { it.toJson() })
else -> tournament.pairables[pid]?.toJson() ?: badRequest("no player with id #${pid}")
} }
} }
override fun put(request: HttpServletRequest): Json { override fun post(request: HttpServletRequest): Json {
val id = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("missing or invalid player selector") val tournament = getTournament(request) ?: badRequest("invalid tournament")
TODO() val payload = getObjectPayload(request)
// player parsing (CB TODO - team handling, based on tournament type)
val player = Player.fromJson(payload)
// CB TODO - handle concurrency
tournament.pairables[player.id] = player
// CB TODO - handle event broadcasting
return Json.Object("success" to true, "id" to player.id)
} }
override fun put(request: HttpServletRequest): Json {
val tournament = getTournament(request) ?: badRequest("invalid tournament")
val id = getSubSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid player selector")
val player = tournament.pairables[id] ?: badRequest("invalid player id")
val payload = getObjectPayload(request)
val updated = Player.fromJson(payload, player as Player)
tournament.pairables[updated.id] = updated
return Json.Object("success" to true)
}
override fun delete(request: HttpServletRequest): Json {
return super.delete(request)
}
} }

View File

@@ -1,43 +0,0 @@
package org.jeudego.pairgoth.api
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest
object RegistrationHandler: ApiHandler {
private fun getTournament(request: HttpServletRequest): Tournament {
val tournamentId = getSelector(request)?.toIntOrNull() ?: badRequest("invalid tournament id")
return Store.getTournament(tournamentId) ?: badRequest("unknown tournament id")
}
override fun get(request: HttpServletRequest): Json {
val tournament = getTournament(request)
return when (val pairableId = getSubSelector(request)?.toIntOrNull()) {
null -> when (val round = request.getParameter("round")?.toIntOrNull()) {
null -> Json.Array(tournament.pairables.map {
Json.Object(
"id" to it.key,
"skip" to Json.Array(it.value)
)
})
else -> Json.Array(tournament.pairables.filter { !it.value.contains(round) }.keys)
}
else -> Json.Array(tournament.pairables[pairableId])
}
}
override fun post(request: HttpServletRequest): Json {
val tournament = getTournament(request)
val payload = getObjectPayload(request)
val pairableId = payload.getInt("id") ?: badRequest("missing player id")
val skip = ( payload.getArray("skip") ?: Json.Array() ).map { Json.TypeUtils.toInt(it) ?: badRequest("invalid round number") }
if (tournament.pairables.contains(pairableId)) badRequest("already registered player: $pairableId")
/* CB TODO - update action for SSE channel */
tournament.pairables[pairableId] = skip.toMutableSet()
return Json.Object("success" to true)
}
}

View File

@@ -0,0 +1,4 @@
package org.jeudego.pairgoth.api
class ResultsHandler: ApiHandler {
}

View File

@@ -0,0 +1,4 @@
package org.jeudego.pairgoth.api
class StandingsHandler: ApiHandler {
}

View File

@@ -2,21 +2,20 @@ package org.jeudego.pairgoth.api
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.model.CanadianByoyomi
import org.jeudego.pairgoth.model.FisherTime
import org.jeudego.pairgoth.model.MacMahon
import org.jeudego.pairgoth.model.Rules
import org.jeudego.pairgoth.model.StandardByoyomi
import org.jeudego.pairgoth.model.SuddenDeath
import org.jeudego.pairgoth.model.TimeSystem
import org.jeudego.pairgoth.model.Tournament import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.TournamentType
import org.jeudego.pairgoth.model.fromJson import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
object TournamentHandler: ApiHandler { object TournamentHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json {
return when (val id = getSelector(request)?.toIntOrNull()) {
null -> Json.Array(Store.getTournamentsIDs())
else -> Store.getTournament(id)?.toJson() ?: badRequest("no tournament with id #${id}")
}
}
override fun post(request: HttpServletRequest): Json { override fun post(request: HttpServletRequest): Json {
val payload = getObjectPayload(request) val payload = getObjectPayload(request)
@@ -28,15 +27,22 @@ object TournamentHandler: ApiHandler {
return Json.Object("success" to true, "id" to tournament.id) return Json.Object("success" to true, "id" to tournament.id)
} }
override fun get(request: HttpServletRequest): Json { override fun put(request: HttpServletRequest): Json {
return when (val id = getSelector(request)?.toIntOrNull()) { val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id")
null -> Json.Array(Store.getTournamentsIDs()) val payload = getObjectPayload(request)
else -> Store.getTournament(id)?.toJson() ?: badRequest("no tournament with id #${id}") // disallow changing type
} if (payload.getString("type")?.let { it != tournament.type.name } == true) badRequest("tournament type cannot be changed")
val updated = Tournament.fromJson(payload, tournament)
updated.pairables.putAll(tournament.pairables)
updated.games.addAll(tournament.games)
updated.criteria.addAll(tournament.criteria)
Store.replaceTournament(updated)
return Json.Object("success" to true)
} }
override fun put(request: HttpServletRequest): Json { override fun delete(request: HttpServletRequest): Json {
val id = getSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid tournament selector") val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id")
TODO() Store.deleteTournament(tournament)
return Json.Object("success" to true)
} }
} }

View File

@@ -0,0 +1,5 @@
package org.jeudego.pairgoth.model
import org.jeudego.pairgoth.store.Store
data class Game(val white: Int, val black: Int, var result: Char = '?', val id: Int = Store.nextGameId)

View File

@@ -1,6 +1,16 @@
package org.jeudego.pairgoth.model package org.jeudego.pairgoth.model
sealed class Pairable(val id: Int, val name: String, val rating: Double, val rank: Int) { import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.store.Store
import kotlin.math.roundToInt
// Pairable
sealed class Pairable(val id: Int, val name: String, open val rating: Double, open val rank: Int) {
abstract fun toJson(): Json.Object
val skip = mutableSetOf<Int>() // skipped rounds
} }
fun Pairable.displayRank(): String = when { fun Pairable.displayRank(): String = when {
@@ -24,3 +34,63 @@ fun Pairable.setRank(rankStr: String): Int {
} }
} }
// Player
class Player(
id: Int,
name: String,
var firstname: String,
rating: Double,
rank: Int,
var country: String,
var club: String
): Pairable(id, name, rating, rank) {
companion object
// used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...)
val externalIds = mutableMapOf<String, String>()
override fun toJson() = Json.Object(
"id" to id,
"name" to name,
"firstname" to firstname,
"rating" to rating,
"rank" to rank,
"country" to country,
"club" to club
)
}
fun Player.Companion.fromJson(json: Json.Object, default: Player? = null) = Player(
id = json.getInt("id") ?: default?.id ?: Store.nextPlayerId,
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
firstname = json.getString("firstname") ?: default?.firstname ?: badRequest("missing firstname"),
rating = json.getDouble("rating") ?: default?.rating ?: badRequest("missing rating"),
rank = json.getInt("rank") ?: default?.rank ?: badRequest("missing rank"),
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
club = json.getString("club") ?: default?.club ?: badRequest("missing club")
)
// Team
class Team(id: Int, name: String): Pairable(id, name, 0.0, 0) {
companion object {}
val players = mutableSetOf<Player>()
override val rating: Double get() = if (players.isEmpty()) super.rating else players.sumOf { player -> player.rating } / players.size
override val rank: Int get() = if (players.isEmpty()) super.rank else (players.sumOf { player -> player.rank.toDouble() } / players.size).roundToInt()
override fun toJson() = Json.Object(
"id" to id,
"name" to name,
"players" to Json.Array(players.map { it.toJson() })
)
}
fun Team.Companion.fromJson(json: Json.Object) = Team(
id = json.getInt("id") ?: Store.nextPlayerId,
name = json.getString("name") ?: badRequest("missing name")
).apply {
json.getArray("players")?.let { arr ->
arr.map {
if (it != null && it is Json.Object) Player.fromJson(it)
else badRequest("invalid players array")
}
} ?: badRequest("missing players")
}

View File

@@ -1,39 +0,0 @@
package org.jeudego.pairgoth.model
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.store.Store
class Player(
id: Int,
name: String,
var firstname: String,
rating: Double,
rank: Int,
var country: String,
var club: String
): Pairable(id, name, rating, rank) {
companion object
// used to store external IDs ("FFG" => FFG ID, "EGF" => EGF PIN, "AGA" => AGA ID ...)
val externalIds = mutableMapOf<String, String>()
}
fun Player.Companion.fromJson(json: Json.Object) = Player(
id = json.getInt("id") ?: Store.nextPlayerId,
name = json.getString("name") ?: badRequest("missing name"),
firstname = json.getString("firstname") ?: badRequest("missing firstname"),
rating = json.getDouble("rating") ?: badRequest("missing rating"),
rank = json.getInt("rank") ?: badRequest("missing rank"),
country = json.getString("country") ?: badRequest("missing country"),
club = json.getString("club") ?: ""
)
fun Player.toJson() = Json.Object(
"id" to id,
"name" to name,
"firstname" to firstname,
"rating" to rating,
"rank" to rank,
"country" to country,
"club" to club
)

View File

@@ -1,6 +1,2 @@
package org.jeudego.pairgoth.model package org.jeudego.pairgoth.model
class Team(id: Int, name: String, rating: Double, rank: Int): Pairable(id, name, rating, rank) {
companion object {}
val players = mutableSetOf<Player>()
}

View File

@@ -6,55 +6,69 @@ import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.store.Store import org.jeudego.pairgoth.store.Store
enum class TournamentType(val playersNumber: Int) {
INDIVIDUAL(1),
PAIRGO(2),
RENGO2(2),
RENGO3(3),
TEAM2(2),
TEAM3(3),
TEAM4(4),
TEAM5(5);
}
data class Tournament( data class Tournament(
var id: Int, val id: Int,
var type: TournamentType, val type: Type,
var name: String, val name: String,
var shortName: String, val shortName: String,
var startDate: LocalDate, val startDate: LocalDate,
var endDate: LocalDate, val endDate: LocalDate,
var country: String, val country: String,
var location: String, val location: String,
var online: Boolean, val online: Boolean,
var timeSystem: TimeSystem, val timeSystem: TimeSystem,
var pairing: Pairing, val pairing: Pairing,
var rules: Rules = Rules.FRENCH, val rules: Rules = Rules.FRENCH,
var gobanSize: Int = 19, val gobanSize: Int = 19,
var komi: Double = 7.5 val komi: Double = 7.5
) { ) {
companion object companion object {}
// player/team id -> set of skipped rounds enum class Type(val playersNumber: Int) {
val pairables = mutableMapOf<Int, MutableSet<Int>>() INDIVIDUAL(1),
PAIRGO(2),
RENGO2(2),
RENGO3(3),
TEAM2(2),
TEAM3(3),
TEAM4(4),
TEAM5(5);
}
enum class Criterion {
NBW, MMS, SOS, SOSOS, SODOS
}
// pairables
val pairables = mutableMapOf<Int, Pairable>()
// games per round
val games = mutableListOf<MutableMap<Int, Game>>()
// standings criteria
val criteria = mutableListOf<Criterion>(
if (pairing.type == Pairing.PairingType.MACMAHON) Criterion.MMS else Criterion.NBW,
Criterion.SOS,
Criterion.SOSOS
)
} }
// Serialization // Serialization
fun Tournament.Companion.fromJson(json: Json.Object) = Tournament( fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament? = null) = Tournament(
id = json.getInt("id") ?: Store.nextTournamentId, id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId,
type = json.getString("type")?.uppercase()?.let { TournamentType.valueOf(it) } ?: badRequest("missing type"), type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type"),
name = json.getString("name") ?: ApiHandler.badRequest("missing name"), name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
shortName = json.getString("shortName") ?: ApiHandler.badRequest("missing shortName"), shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
startDate = json.getLocalDate("startDate") ?: ApiHandler.badRequest("missing startDate"), startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"),
endDate = json.getLocalDate("endDate") ?: ApiHandler.badRequest("missing endDate"), endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"),
country = json.getString("country") ?: ApiHandler.badRequest("missing country"), country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
location = json.getString("location") ?: ApiHandler.badRequest("missing location"), location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
online = json.getBoolean("online") ?: false, online = json.getBoolean("online") ?: default?.online ?: false,
komi = json.getDouble("komi") ?: 7.5, komi = json.getDouble("komi") ?: default?.komi ?: 7.5,
rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: Rules.FRENCH, rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH,
gobanSize = json.getInt("gobanSize") ?: 19, gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
timeSystem = TimeSystem.fromJson(json.getObject("timeSystem") ?: badRequest("missing timeSystem")), timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
pairing = MacMahon() pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing")
) )
fun Tournament.toJson() = Json.Object( fun Tournament.toJson() = Json.Object(

View File

@@ -5,14 +5,19 @@ import org.jeudego.pairgoth.model.Tournament
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.E import kotlin.math.E
// CB TODO - handle concurrency:
// - either with concurrent maps
// - or with a thread isolation (better, covers more operations)
object Store { object Store {
private val _nextTournamentId = AtomicInteger() private val _nextTournamentId = AtomicInteger()
private val _nextPlayerId = AtomicInteger() private val _nextPlayerId = AtomicInteger()
private val _nextGameId = AtomicInteger()
val nextTournamentId get() = _nextTournamentId.incrementAndGet() val nextTournamentId get() = _nextTournamentId.incrementAndGet()
val nextPlayerId get() = _nextPlayerId.incrementAndGet() val nextPlayerId get() = _nextPlayerId.incrementAndGet()
val nextGameId get() = _nextGameId.incrementAndGet()
private val tournaments = mutableMapOf<Int, Tournament>() private val tournaments = mutableMapOf<Int, Tournament>()
private val players = mutableMapOf<Int, Player>()
fun addTournament(tournament: Tournament) { fun addTournament(tournament: Tournament) {
if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists") if (tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} already exists")
@@ -23,12 +28,14 @@ object Store {
fun getTournamentsIDs(): Set<Int> = tournaments.keys fun getTournamentsIDs(): Set<Int> = tournaments.keys
fun addPlayer(player: Player) { fun replaceTournament(tournament: Tournament) {
if (players.containsKey(player.id)) throw Error("player id #${player.id} already exists") if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
players[player.id] = player tournaments[tournament.id] = tournament
} }
fun getPlayer(id: Int) = players[id] fun deleteTournament(tournament: Tournament) {
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
tournaments.remove(tournament.id)
fun getPlayersIDs(): Set<Int> = players.keys }
} }

View File

@@ -2,7 +2,7 @@ package org.jeudego.pairgoth.web
import com.republicate.kson.Json import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.RegistrationHandler import org.jeudego.pairgoth.api.PairingHandler
import org.jeudego.pairgoth.api.PlayerHandler import org.jeudego.pairgoth.api.PlayerHandler
import org.jeudego.pairgoth.api.TournamentHandler import org.jeudego.pairgoth.api.TournamentHandler
import org.jeudego.pairgoth.util.Colorizer.green import org.jeudego.pairgoth.util.Colorizer.green
@@ -63,10 +63,11 @@ class ApiServlet : HttpServlet() {
// choose handler // choose handler
val handler = when (entity) { val handler = when (entity) {
"tournament" -> "tour" ->
when (subEntity) { when (subEntity) {
null -> TournamentHandler null -> TournamentHandler
"registration" -> RegistrationHandler "part" -> PlayerHandler
"pair" -> PairingHandler
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity") else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
} }
"player" -> PlayerHandler "player" -> PlayerHandler
@@ -223,8 +224,8 @@ class ApiServlet : HttpServlet() {
} }
companion object { companion object {
protected var logger = LoggerFactory.getLogger("api") private var logger = LoggerFactory.getLogger("api")
protected const val EXPECTED_CHARSET = "utf8" private const val EXPECTED_CHARSET = "utf8"
const val AUTH_HEADER = "Authorization" const val AUTH_HEADER = "Authorization"
const val AUTH_PREFIX = "Bearer" const val AUTH_PREFIX = "Bearer"
private fun isJson(mimeType: String): Boolean { private fun isJson(mimeType: String): Boolean {