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" \
--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" } }' \
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" \
--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
http://localhost:8080/api/tour/1/part
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
import com.republicate.kson.Json
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
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
object PlayerHandler: ApiHandler {
object PlayerHandler: PairgothApiHandler {
override fun get(request: HttpServletRequest): Json {
val tournament = getTournament(request) ?: badRequest("invalid tournament")
return when (val pid = getSubSelector(request)?.toIntOrNull()) {
null -> Json.Array(tournament.pairables.values.map { it.toJson() })
else -> tournament.pairables[pid]?.toJson() ?: badRequest("no player with id #${pid}")
}
}
override fun post(request: HttpServletRequest): Json {
val tournament = getTournament(request) ?: badRequest("invalid tournament")
val payload = getObjectPayload(request)
// player parsing
// player parsing (CB TODO - team handling, based on tournament type)
val player = Player.fromJson(payload)
Store.addPlayer(player)
// 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 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()
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 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.TournamentType
import org.jeudego.pairgoth.model.fromJson
import org.jeudego.pairgoth.model.toJson
import org.jeudego.pairgoth.store.Store
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 {
val payload = getObjectPayload(request)
@@ -28,15 +27,22 @@ object TournamentHandler: ApiHandler {
return Json.Object("success" to true, "id" to tournament.id)
}
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 put(request: HttpServletRequest): Json {
val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id")
val payload = getObjectPayload(request)
// 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 {
val id = getSelector(request)?.toIntOrNull() ?: badRequest("missing or invalid tournament selector")
TODO()
override fun delete(request: HttpServletRequest): Json {
val tournament = getTournament(request) ?: badRequest("missing or invalid tournament id")
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
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 {
@@ -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
class Team(id: Int, name: String, rating: Double, rank: Int): Pairable(id, name, rating, rank) {
companion object {}
val players = mutableSetOf<Player>()
}

View File

@@ -6,7 +6,24 @@ import org.jeudego.pairgoth.api.ApiHandler
import org.jeudego.pairgoth.api.ApiHandler.Companion.badRequest
import org.jeudego.pairgoth.store.Store
enum class TournamentType(val playersNumber: Int) {
data class Tournament(
val id: Int,
val type: Type,
val name: String,
val shortName: String,
val startDate: LocalDate,
val endDate: LocalDate,
val country: String,
val location: String,
val online: Boolean,
val timeSystem: TimeSystem,
val pairing: Pairing,
val rules: Rules = Rules.FRENCH,
val gobanSize: Int = 19,
val komi: Double = 7.5
) {
companion object {}
enum class Type(val playersNumber: Int) {
INDIVIDUAL(1),
PAIRGO(2),
RENGO2(2),
@@ -15,46 +32,43 @@ enum class TournamentType(val playersNumber: Int) {
TEAM3(3),
TEAM4(4),
TEAM5(5);
}
}
data class Tournament(
var id: Int,
var type: TournamentType,
var name: String,
var shortName: String,
var startDate: LocalDate,
var endDate: LocalDate,
var country: String,
var location: String,
var online: Boolean,
var timeSystem: TimeSystem,
var pairing: Pairing,
var rules: Rules = Rules.FRENCH,
var gobanSize: Int = 19,
var komi: Double = 7.5
) {
companion object
// player/team id -> set of skipped rounds
val pairables = mutableMapOf<Int, MutableSet<Int>>()
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
fun Tournament.Companion.fromJson(json: Json.Object) = Tournament(
id = json.getInt("id") ?: Store.nextTournamentId,
type = json.getString("type")?.uppercase()?.let { TournamentType.valueOf(it) } ?: badRequest("missing type"),
name = json.getString("name") ?: ApiHandler.badRequest("missing name"),
shortName = json.getString("shortName") ?: ApiHandler.badRequest("missing shortName"),
startDate = json.getLocalDate("startDate") ?: ApiHandler.badRequest("missing startDate"),
endDate = json.getLocalDate("endDate") ?: ApiHandler.badRequest("missing endDate"),
country = json.getString("country") ?: ApiHandler.badRequest("missing country"),
location = json.getString("location") ?: ApiHandler.badRequest("missing location"),
online = json.getBoolean("online") ?: false,
komi = json.getDouble("komi") ?: 7.5,
rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: Rules.FRENCH,
gobanSize = json.getInt("gobanSize") ?: 19,
timeSystem = TimeSystem.fromJson(json.getObject("timeSystem") ?: badRequest("missing timeSystem")),
pairing = MacMahon()
fun Tournament.Companion.fromJson(json: Json.Object, default: Tournament? = null) = Tournament(
id = json.getInt("id") ?: default?.id ?: Store.nextTournamentId,
type = json.getString("type")?.uppercase()?.let { Tournament.Type.valueOf(it) } ?: default?.type ?: badRequest("missing type"),
name = json.getString("name") ?: default?.name ?: badRequest("missing name"),
shortName = json.getString("shortName") ?: default?.shortName ?: badRequest("missing shortName"),
startDate = json.getLocalDate("startDate") ?: default?.startDate ?: badRequest("missing startDate"),
endDate = json.getLocalDate("endDate") ?: default?.endDate ?: badRequest("missing endDate"),
country = json.getString("country") ?: default?.country ?: badRequest("missing country"),
location = json.getString("location") ?: default?.location ?: badRequest("missing location"),
online = json.getBoolean("online") ?: default?.online ?: false,
komi = json.getDouble("komi") ?: default?.komi ?: 7.5,
rules = json.getString("rules")?.let { Rules.valueOf(it) } ?: default?.rules ?: Rules.FRENCH,
gobanSize = json.getInt("gobanSize") ?: default?.gobanSize ?: 19,
timeSystem = json.getObject("timeSystem")?.let { TimeSystem.fromJson(it) } ?: default?.timeSystem ?: badRequest("missing timeSystem"),
pairing = json.getObject("pairing")?.let { Pairing.fromJson(it) } ?: default?.pairing ?: badRequest("missing pairing")
)
fun Tournament.toJson() = Json.Object(

View File

@@ -5,14 +5,19 @@ import org.jeudego.pairgoth.model.Tournament
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.E
// CB TODO - handle concurrency:
// - either with concurrent maps
// - or with a thread isolation (better, covers more operations)
object Store {
private val _nextTournamentId = AtomicInteger()
private val _nextPlayerId = AtomicInteger()
private val _nextGameId = AtomicInteger()
val nextTournamentId get() = _nextTournamentId.incrementAndGet()
val nextPlayerId get() = _nextPlayerId.incrementAndGet()
val nextGameId get() = _nextGameId.incrementAndGet()
private val tournaments = mutableMapOf<Int, Tournament>()
private val players = mutableMapOf<Int, Player>()
fun addTournament(tournament: Tournament) {
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 addPlayer(player: Player) {
if (players.containsKey(player.id)) throw Error("player id #${player.id} already exists")
players[player.id] = player
fun replaceTournament(tournament: Tournament) {
if (!tournaments.containsKey(tournament.id)) throw Error("tournament id #${tournament.id} not known")
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 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.TournamentHandler
import org.jeudego.pairgoth.util.Colorizer.green
@@ -63,10 +63,11 @@ class ApiServlet : HttpServlet() {
// choose handler
val handler = when (entity) {
"tournament" ->
"tour" ->
when (subEntity) {
null -> TournamentHandler
"registration" -> RegistrationHandler
"part" -> PlayerHandler
"pair" -> PairingHandler
else -> ApiHandler.badRequest("unknown sub-entity: $subEntity")
}
"player" -> PlayerHandler
@@ -223,8 +224,8 @@ class ApiServlet : HttpServlet() {
}
companion object {
protected var logger = LoggerFactory.getLogger("api")
protected const val EXPECTED_CHARSET = "utf8"
private var logger = LoggerFactory.getLogger("api")
private const val EXPECTED_CHARSET = "utf8"
const val AUTH_HEADER = "Authorization"
const val AUTH_PREFIX = "Bearer"
private fun isJson(mimeType: String): Boolean {