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 #!/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" \ 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/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 { 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? { fun getSelector(request: HttpServletRequest): String? {
return request.getAttribute(SELECTOR_KEY) as String? return request.getAttribute(SELECTOR_KEY) as String?
} }
fun getSubSelector(request: HttpServletRequest): String? {
return request.getAttribute(SUBSELECTOR_KEY) as String?
}
companion object { companion object {
const val PAYLOAD_KEY = "PAYLOAD" const val PAYLOAD_KEY = "PAYLOAD"
const val SELECTOR_KEY = "SELECTOR" const val SELECTOR_KEY = "SELECTOR"
const val SUBSELECTOR_KEY = "SUBSELECTOR"
val logger = LoggerFactory.getLogger("api") val logger = LoggerFactory.getLogger("api")
fun badRequest(msg: String = "bad request"): Nothing = throw ApiException(HttpServletResponse.SC_BAD_REQUEST, msg) 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.Player
import org.jeudego.pairgoth.model.Tournament 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 org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
class PlayerHandler: ApiHandler { object PlayerHandler: ApiHandler {
override fun post(request: HttpServletRequest): Json { override fun post(request: HttpServletRequest): Json {
val json = getPayload(request) val payload = getObjectPayload(request)
if (!json.isObject) ApiHandler.badRequest("expecting a json object")
val payload = json.asObject()
// player parsing // player parsing
val player = Player.fromJson(payload) val player = Player.fromJson(payload)
@@ -20,4 +19,17 @@ class PlayerHandler: ApiHandler {
Store.addPlayer(player) Store.addPlayer(player)
return Json.Object("success" to true, "id" to player.id) 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 org.jeudego.pairgoth.store.Store
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
class TournamentHandler(): ApiHandler { object TournamentHandler: ApiHandler {
override fun post(request: HttpServletRequest): Json { override fun post(request: HttpServletRequest): Json {
val json = getPayload(request) val payload = getObjectPayload(request)
if (!json.isObject) badRequest("expecting a json object")
val payload = json.asObject()
// tournament parsing // tournament parsing
val tournament = Tournament.fromJson(payload) val tournament = Tournament.fromJson(payload)

View File

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

View File

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

View File

@@ -22,8 +22,13 @@ object Store {
fun getTournament(id: Int) = tournaments[id] fun getTournament(id: Int) = tournaments[id]
fun getTournamentsIDs(): Set<Int> = tournaments.keys fun getTournamentsIDs(): Set<Int> = tournaments.keys
fun addPlayer(player: Player) { fun addPlayer(player: Player) {
if (players.containsKey(player.id)) throw Error("player id #${player.id} already exists") if (players.containsKey(player.id)) throw Error("player id #${player.id} already exists")
players[player.id] = player 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 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.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
import org.jeudego.pairgoth.util.Colorizer.green import org.jeudego.pairgoth.util.Colorizer.green
import org.jeudego.pairgoth.util.Colorizer.red import org.jeudego.pairgoth.util.Colorizer.red
import org.jeudego.pairgoth.util.parse import org.jeudego.pairgoth.util.parse
import org.jeudego.pairgoth.util.toString import org.jeudego.pairgoth.util.toString
import org.jeudego.pairgoth.web.ApiException
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.IOException import java.io.IOException
import java.io.StringWriter
import java.util.* import java.util.*
import javax.servlet.ServletException
import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
class ApiServlet : HttpServlet() { class ApiServlet : HttpServlet() {
val tournamentHandler = TournamentHandler()
val playerHandler = PlayerHandler()
public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { public override fun doGet(request: HttpServletRequest, response: HttpServletResponse) {
doRequest(request, response) doRequest(request, response)
} }
@@ -47,29 +41,47 @@ class ApiServlet : HttpServlet() {
var payload: Json? = null var payload: Json? = null
var reason = "OK" var reason = "OK"
try { try {
// validate request
if ("dev" == WebappManager.getProperty("webapp.env")) { if ("dev" == WebappManager.getProperty("webapp.env")) {
response.addHeader("Access-Control-Allow-Origin", "*") response.addHeader("Access-Control-Allow-Origin", "*")
} }
validateContentType(request) validateContentType(request)
validateAccept(request); validateAccept(request);
val parts = uri.split("/".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() // parse request URI
if (parts.size < 3 || parts.size > 5) throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
if (parts.size >= 4) { val parts = uri.split("/").filter { !it.isEmpty() }
request.setAttribute(ApiHandler.SELECTOR_KEY, parts[3]) if (parts.size !in 2..5 || parts[0] != "api") throw ApiException(HttpServletResponse.SC_BAD_REQUEST)
}
val entity = parts[2] 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) { val handler = when (entity) {
"tournament" -> tournamentHandler "tournament" ->
"player" -> playerHandler when (subEntity) {
else -> ApiHandler.badRequest("unknown entity") 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) payload = handler.route(request, response)
// if payload is null, it means the handler already sent the response // if payload is null, it means the handler already sent the response
if (payload != null) { if (payload != null) {
setContentType(response) setContentType(response)
payload.toString(response.writer) payload.toString(response.writer)
} }
} catch (apiException: ApiException) { } catch (apiException: ApiException) {
reason = apiException.message ?: "unknown API error" reason = apiException.message ?: "unknown API error"
if (reason == null) error(response, apiException.code) else 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 level = trace