the api surface

This commit is contained in:
2025-09-22 21:51:26 +03:00
parent ff8059f078
commit 20a3e72ac4
+165
View File
@@ -0,0 +1,165 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use cruciverb_core::grid::Cell;
use cruciverb_core::puzzle::Puzzle;
// -- request types --
#[derive(Deserialize)]
pub struct GenerateRequest {
pub difficulty: Option<String>,
pub size: Option<usize>,
pub density: Option<f64>,
pub clue_difficulty: Option<u8>,
pub seed: Option<u64>,
pub theme: Option<String>,
pub cryptic: Option<bool>,
pub pack: Option<String>,
}
#[derive(Deserialize)]
pub struct ValidateRequest {
pub cells: Vec<Vec<Option<String>>>,
pub elapsed: Option<i64>,
// kept for client compatibility; the server now uses its own clock for streaks
#[allow(dead_code)]
pub local_date: Option<String>,
pub room_code: Option<String>,
pub cell_results: Option<String>,
}
#[derive(Deserialize)]
pub struct HintRequest {
pub kind: String,
pub row: usize,
pub col: usize,
pub direction: Option<String>,
pub letter: Option<String>,
}
#[derive(Deserialize)]
pub struct ProgressRequest {
pub cells: Vec<Vec<Option<String>>>,
}
#[derive(Deserialize)]
pub struct UpdateMeRequest {
pub display_name: Option<String>,
}
// -- response types --
#[derive(Serialize)]
pub struct GenerateResponse {
pub id: String,
pub short_id: String,
pub puzzle: ClientPuzzle,
}
#[derive(Serialize)]
pub struct ClientPuzzleResponse {
pub id: String,
pub short_id: String,
pub puzzle: ClientPuzzle,
}
#[derive(Serialize)]
pub struct ClientPuzzle {
pub width: usize,
pub height: usize,
pub cells: Vec<Vec<ClientCell>>,
pub clues_across: Vec<ClientClue>,
pub clues_down: Vec<ClientClue>,
pub difficulty_score: u32,
pub cell_numbers: HashMap<String, u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub theme: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub theme_entries: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ClientCell {
Black,
Empty,
}
#[derive(Serialize)]
pub struct ClientClue {
pub number: u32,
pub direction: String,
pub text: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub definition: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wordplay_type: Option<String>,
}
#[derive(Serialize)]
#[serde(untagged)]
pub enum HintResponse {
CheckCell { is_correct: bool },
RevealLetter { row: usize, col: usize, letter: String },
RevealWord { letters: Vec<LetterPosition> },
}
#[derive(Serialize)]
pub struct LetterPosition {
pub row: usize,
pub col: usize,
pub letter: String,
}
// -- conversions --
pub fn to_client_puzzle(puzzle: &Puzzle, _id: Uuid) -> ClientPuzzle {
let mut cells = Vec::with_capacity(puzzle.height);
for r in 0..puzzle.height {
let mut row = Vec::with_capacity(puzzle.width);
for c in 0..puzzle.width {
match puzzle.grid.get(r, c) {
Cell::Black => row.push(ClientCell::Black),
_ => row.push(ClientCell::Empty),
}
}
cells.push(row);
}
let mut cell_numbers: HashMap<String, u32> = HashMap::new();
for word in &puzzle.words {
let key = format!("{},{}", word.row, word.col);
cell_numbers.entry(key).or_insert(word.clue_number);
}
let clues_across = puzzle.clues_across.iter().map(|c| ClientClue {
number: c.number,
direction: "across".into(),
text: c.text.clone(),
definition: c.definition.clone(),
wordplay_type: c.wordplay_type.clone(),
}).collect();
let clues_down = puzzle.clues_down.iter().map(|c| ClientClue {
number: c.number,
direction: "down".into(),
text: c.text.clone(),
definition: c.definition.clone(),
wordplay_type: c.wordplay_type.clone(),
}).collect();
ClientPuzzle {
width: puzzle.width,
height: puzzle.height,
cells,
clues_across,
clues_down,
difficulty_score: puzzle.difficulty_score,
cell_numbers,
theme: puzzle.theme.clone(),
theme_entries: puzzle.theme_entries.clone(),
}
}