Players registration

This commit is contained in:
Claude Brisson
2023-05-14 16:34:02 +02:00
parent fd67d4e044
commit c37023405f
10 changed files with 140 additions and 34 deletions

24
test.sh
View File

@@ -1,11 +1,27 @@
#!/bin/bash
curl -s -D - --header "Accept: application/json" http://localhost:8080/api/tournament
curl -s --header "Accept: application/json" --header "Content-Type: application/json" \
--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
curl -s -D - --header "Accept: application/json" http://localhost:8080/api/tournament/1
curl -s --header "Accept: application/json" http://localhost:8080/api/tournament
curl -s --header "Accept: application/json" http://localhost:8080/api/tournament/1
curl -s --header "Accept: application/json" --header "Content-Type: application/json" \
--request POST \
--data '{ "name": "Burma", "firstname": "Nestor", "rating": 1600, "rank": -2, "country": "FR", "club": "13Ma" }' \
http://localhost:8080/api/player
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

View File

@@ -38,16 +38,33 @@ interface ApiHandler {
}
fun getPayload(request: HttpServletRequest): Json {
return request.getAttribute(PAYLOAD_KEY) as Json ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
return request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
}
fun getObjectPayload(request: HttpServletRequest): Json.Object {
val json = request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
if (!json.isObject) badRequest("expecting a json object")
return json.asObject()
}
fun getArrayPayload(request: HttpServletRequest): Json.Array {
val json = request.getAttribute(PAYLOAD_KEY) as Json? ?: throw ApiException(HttpServletResponse.SC_BAD_REQUEST, "no payload")
if (!json.isArray) badRequest("expecting a json array")
return json.asArray()
}
fun getSelector(request: HttpServletRequest): String? {
return request.getAttribute(SELECTOR_KEY) as String?
}
fun getSubSelector(request: HttpServletRequest): String? {
return request.getAttribute(SUBSELECTOR_KEY) as String?
}
companion object {
const val PAYLOAD_KEY = "PAYLOAD"
const val SELECTOR_KEY = "SELECTOR"
const val SUBSELECTOR_KEY = "SUBSELECTOR"
val logger = LoggerFactory.getLogger("api")
fun badRequest(msg: String = "bad request"): Nothing = throw ApiException(HttpServletResponse.SC_BAD_REQUEST, msg)
}

View File

@@ -4,15 +4,14 @@ import com.republicate.kson.Json
import org.jeudego.pairgoth.model.Player
import org.jeudego.pairgoth.model.Tournament
import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest
class PlayerHandler: ApiHandler {
object PlayerHandler: ApiHandler {
override fun post(request: HttpServletRequest): Json {
val json = getPayload(request)
if (!json.isObject) ApiHandler.badRequest("expecting a json object")
val payload = json.asObject()
val payload = getObjectPayload(request)
// player parsing
val player = Player.fromJson(payload)
@@ -20,4 +19,17 @@ class PlayerHandler: ApiHandler {
Store.addPlayer(player)
return Json.Object("success" to true, "id" to player.id)
}
}
override fun get(request: HttpServletRequest): Json {
return when (val id = getSelector(request)?.toIntOrNull()) {
null -> Json.Array(Store.getPlayersIDs())
else -> Store.getPlayer(id)?.toJson() ?: ApiHandler.badRequest("no player with id #${id}")
}
}
override fun put(request: HttpServletRequest): Json {
val id = getSelector(request)?.toIntOrNull() ?: ApiHandler.badRequest("missing or invalid player selector")
TODO()
}
}

View File

@@ -0,0 +1,43 @@
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

@@ -16,12 +16,10 @@ import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest
class TournamentHandler(): ApiHandler {
object TournamentHandler: ApiHandler {
override fun post(request: HttpServletRequest): Json {
val json = getPayload(request)
if (!json.isObject) badRequest("expecting a json object")
val payload = json.asObject()
val payload = getObjectPayload(request)
// tournament parsing
val tournament = Tournament.fromJson(payload)

View File

@@ -1,6 +1,7 @@
package org.jeudego.pairgoth.model
sealed class Pairable(val id: Int, val name: String, val rating: Double, val rank: Int)
sealed class Pairable(val id: Int, val name: String, val rating: Double, val rank: Int) {
}
fun Pairable.displayRank(): String = when {
rank < 0 -> "${-rank}k"
@@ -22,3 +23,4 @@ fun Pairable.setRank(rankStr: String): Int {
else -> throw Error("impossible")
}
}

View File

@@ -33,8 +33,9 @@ data class Tournament(
var gobanSize: Int = 19,
var komi: Double = 7.5
) {
companion object {}
val pairables = mutableMapOf<Int, Pairable>()
companion object
// player/team id -> set of skipped rounds
val pairables = mutableMapOf<Int, MutableSet<Int>>()
}
// Serialization

View File

@@ -22,8 +22,13 @@ object Store {
fun getTournament(id: Int) = tournaments[id]
fun getTournamentsIDs(): Set<Int> = tournaments.keys
fun addPlayer(player: Player) {
if (players.containsKey(player.id)) throw Error("player id #${player.id} already exists")
players[player.id] = player
}
fun getPlayer(id: Int) = players[id]
fun getPlayersIDs(): Set<Int> = players.keys
}

View File

@@ -2,28 +2,22 @@ package org.jeudego.pairgoth.web
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.RegistrationHandler
import org.jeudego.pairgoth.api.PlayerHandler
import org.jeudego.pairgoth.api.TournamentHandler
import org.jeudego.pairgoth.util.Colorizer
import org.jeudego.pairgoth.util.Colorizer.green
import org.jeudego.pairgoth.util.Colorizer.red
import org.jeudego.pairgoth.util.parse
import org.jeudego.pairgoth.util.toString
import org.jeudego.pairgoth.web.ApiException
import org.slf4j.LoggerFactory
import java.io.IOException
import java.io.StringWriter
import java.util.*
import javax.servlet.ServletException
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
class ApiServlet : HttpServlet() {
val tournamentHandler = TournamentHandler()
val playerHandler = PlayerHandler()
public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
doRequest(request, response)
}
@@ -47,29 +41,47 @@ class ApiServlet : HttpServlet() {
var payload: Json? = null
var reason = "OK"
try {
// validate request
if ("dev" == WebappManager.getProperty("webapp.env")) {
response.addHeader("Access-Control-Allow-Origin", "*")
}
validateContentType(request)
validateAccept(request);
val parts = uri.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
if (parts.size < 3 || parts.size > 5) throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
if (parts.size >= 4) {
request.setAttribute(ApiHandler.SELECTOR_KEY, parts[3])
}
val entity = parts[2]
// parse request URI
val parts = uri.split("/").filter { !it.isEmpty() }
if (parts.size !in 2..5 || parts[0] != "api") throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
val entity = parts[1]
val selector = parts.getOrNull(2)?.also { request.setAttribute(ApiHandler.SELECTOR_KEY, it) }
val subEntity = parts.getOrNull(3)
val subSelector = parts.getOrNull(4)?.also { request.setAttribute(ApiHandler.SUBSELECTOR_KEY, it) }
// choose handler
val handler = when (entity) {
"tournament" -> tournamentHandler
"player" -> playerHandler
else -> ApiHandler.badRequest("unknown entity")
"tournament" ->
when (subEntity) {
null -> TournamentHandler
"registration" -> RegistrationHandler
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
}
"player" -> PlayerHandler
else -> ApiHandler.badRequest("unknown entity: $entity")
}
// call handler
payload = handler.route(request, response)
// if payload is null, it means the handler already sent the response
if (payload != null) {
setContentType(response)
payload.toString(response.writer)
}
} catch (apiException: ApiException) {
reason = apiException.message ?: "unknown API error"
if (reason == null) error(response, apiException.code) else error(

View File

@@ -1,2 +1,2 @@
format = %date [%level] %ip [%logger] %message (@%file:%line:%column)
format = [%level] %ip [%logger] %message (@%file:%line:%column)
level = trace