1use pyo3::prelude::*;
4
5use ::eulumdat_quiz as quiz_core;
6
7#[pyclass(eq, eq_int)]
12#[derive(Clone, Copy, PartialEq, Eq)]
13pub enum Category {
14 EulumdatFormat = 0,
15 IesFormat = 1,
16 Symmetry = 2,
17 CoordinateSystems = 3,
18 PhotometricCalc = 4,
19 BugRating = 5,
20 UgrGlare = 6,
21 ColorScience = 7,
22 Horticultural = 8,
23 BimIntegration = 9,
24 ModernFormats = 10,
25 Validation = 11,
26 Units = 12,
27 DiagramTypes = 13,
28 DiagramReading = 14,
29 Standards = 15,
30}
31
32#[pymethods]
33impl Category {
34 fn key(&self) -> &'static str {
36 self.to_core().key()
37 }
38
39 fn label(&self) -> &'static str {
41 self.to_core().label()
42 }
43
44 fn __repr__(&self) -> String {
45 format!("Category.{}", self.label())
46 }
47}
48
49impl Category {
50 fn to_core(self) -> quiz_core::Category {
51 match self {
52 Self::EulumdatFormat => quiz_core::Category::EulumdatFormat,
53 Self::IesFormat => quiz_core::Category::IesFormat,
54 Self::Symmetry => quiz_core::Category::Symmetry,
55 Self::CoordinateSystems => quiz_core::Category::CoordinateSystems,
56 Self::PhotometricCalc => quiz_core::Category::PhotometricCalc,
57 Self::BugRating => quiz_core::Category::BugRating,
58 Self::UgrGlare => quiz_core::Category::UgrGlare,
59 Self::ColorScience => quiz_core::Category::ColorScience,
60 Self::Horticultural => quiz_core::Category::Horticultural,
61 Self::BimIntegration => quiz_core::Category::BimIntegration,
62 Self::ModernFormats => quiz_core::Category::ModernFormats,
63 Self::Validation => quiz_core::Category::Validation,
64 Self::Units => quiz_core::Category::Units,
65 Self::DiagramTypes => quiz_core::Category::DiagramTypes,
66 Self::DiagramReading => quiz_core::Category::DiagramReading,
67 Self::Standards => quiz_core::Category::Standards,
68 }
69 }
70
71 fn from_core(c: quiz_core::Category) -> Self {
72 match c {
73 quiz_core::Category::EulumdatFormat => Self::EulumdatFormat,
74 quiz_core::Category::IesFormat => Self::IesFormat,
75 quiz_core::Category::Symmetry => Self::Symmetry,
76 quiz_core::Category::CoordinateSystems => Self::CoordinateSystems,
77 quiz_core::Category::PhotometricCalc => Self::PhotometricCalc,
78 quiz_core::Category::BugRating => Self::BugRating,
79 quiz_core::Category::UgrGlare => Self::UgrGlare,
80 quiz_core::Category::ColorScience => Self::ColorScience,
81 quiz_core::Category::Horticultural => Self::Horticultural,
82 quiz_core::Category::BimIntegration => Self::BimIntegration,
83 quiz_core::Category::ModernFormats => Self::ModernFormats,
84 quiz_core::Category::Validation => Self::Validation,
85 quiz_core::Category::Units => Self::Units,
86 quiz_core::Category::DiagramTypes => Self::DiagramTypes,
87 quiz_core::Category::DiagramReading => Self::DiagramReading,
88 quiz_core::Category::Standards => Self::Standards,
89 }
90 }
91}
92
93#[pyclass(eq, eq_int)]
98#[derive(Clone, Copy, PartialEq, Eq)]
99pub enum Difficulty {
100 Beginner = 0,
101 Intermediate = 1,
102 Expert = 2,
103}
104
105#[pymethods]
106impl Difficulty {
107 fn key(&self) -> &'static str {
108 self.to_core().key()
109 }
110
111 fn label(&self) -> &'static str {
112 self.to_core().label()
113 }
114
115 fn __repr__(&self) -> String {
116 format!("Difficulty.{}", self.label())
117 }
118}
119
120impl Difficulty {
121 fn to_core(self) -> quiz_core::Difficulty {
122 match self {
123 Self::Beginner => quiz_core::Difficulty::Beginner,
124 Self::Intermediate => quiz_core::Difficulty::Intermediate,
125 Self::Expert => quiz_core::Difficulty::Expert,
126 }
127 }
128
129 fn from_core(d: quiz_core::Difficulty) -> Self {
130 match d {
131 quiz_core::Difficulty::Beginner => Self::Beginner,
132 quiz_core::Difficulty::Intermediate => Self::Intermediate,
133 quiz_core::Difficulty::Expert => Self::Expert,
134 }
135 }
136}
137
138#[pyclass]
143#[derive(Clone)]
144pub struct Question {
145 inner: quiz_core::Question,
146}
147
148#[pymethods]
149impl Question {
150 #[getter]
151 fn id(&self) -> u32 {
152 self.inner.id
153 }
154
155 #[getter]
156 fn category(&self) -> Category {
157 Category::from_core(self.inner.category)
158 }
159
160 #[getter]
161 fn difficulty(&self) -> Difficulty {
162 Difficulty::from_core(self.inner.difficulty)
163 }
164
165 #[getter]
166 fn text(&self) -> &str {
167 &self.inner.text
168 }
169
170 #[getter]
171 fn options(&self) -> Vec<String> {
172 self.inner.options.clone()
173 }
174
175 #[getter]
176 fn correct_index(&self) -> u8 {
177 self.inner.correct_index
178 }
179
180 #[getter]
181 fn explanation(&self) -> &str {
182 &self.inner.explanation
183 }
184
185 #[getter]
186 fn reference(&self) -> Option<String> {
187 self.inner.reference.clone()
188 }
189
190 fn __repr__(&self) -> String {
191 format!(
192 "Question(id={}, category={}, difficulty={})",
193 self.inner.id,
194 self.inner.category.label(),
195 self.inner.difficulty.label()
196 )
197 }
198}
199
200#[pyclass]
205#[derive(Clone)]
206pub struct AnswerResult {
207 inner: quiz_core::AnswerResult,
208}
209
210#[pymethods]
211impl AnswerResult {
212 #[getter]
213 fn is_correct(&self) -> bool {
214 self.inner.is_correct
215 }
216
217 #[getter]
218 fn correct_index(&self) -> u8 {
219 self.inner.correct_index
220 }
221
222 #[getter]
223 fn explanation(&self) -> &str {
224 &self.inner.explanation
225 }
226
227 #[getter]
228 fn reference(&self) -> Option<String> {
229 self.inner.reference.clone()
230 }
231
232 fn __repr__(&self) -> String {
233 if self.inner.is_correct {
234 "AnswerResult(correct=True)".to_string()
235 } else {
236 format!(
237 "AnswerResult(correct=False, correct_index={})",
238 self.inner.correct_index
239 )
240 }
241 }
242}
243
244#[pyclass]
249#[derive(Clone)]
250pub struct CategoryScore {
251 inner: quiz_core::CategoryScore,
252}
253
254#[pymethods]
255impl CategoryScore {
256 #[getter]
257 fn category(&self) -> Category {
258 Category::from_core(self.inner.category)
259 }
260
261 #[getter]
262 fn correct(&self) -> u32 {
263 self.inner.correct
264 }
265
266 #[getter]
267 fn total(&self) -> u32 {
268 self.inner.total
269 }
270
271 fn percentage(&self) -> f64 {
272 if self.inner.total == 0 {
273 0.0
274 } else {
275 self.inner.correct as f64 / self.inner.total as f64 * 100.0
276 }
277 }
278
279 fn __repr__(&self) -> String {
280 format!(
281 "CategoryScore({}: {}/{})",
282 self.inner.category.label(),
283 self.inner.correct,
284 self.inner.total
285 )
286 }
287}
288
289#[pyclass]
290#[derive(Clone)]
291pub struct DifficultyScore {
292 inner: quiz_core::DifficultyScore,
293}
294
295#[pymethods]
296impl DifficultyScore {
297 #[getter]
298 fn difficulty(&self) -> Difficulty {
299 Difficulty::from_core(self.inner.difficulty)
300 }
301
302 #[getter]
303 fn correct(&self) -> u32 {
304 self.inner.correct
305 }
306
307 #[getter]
308 fn total(&self) -> u32 {
309 self.inner.total
310 }
311
312 fn percentage(&self) -> f64 {
313 if self.inner.total == 0 {
314 0.0
315 } else {
316 self.inner.correct as f64 / self.inner.total as f64 * 100.0
317 }
318 }
319
320 fn __repr__(&self) -> String {
321 format!(
322 "DifficultyScore({}: {}/{})",
323 self.inner.difficulty.label(),
324 self.inner.correct,
325 self.inner.total
326 )
327 }
328}
329
330#[pyclass]
335#[derive(Clone)]
336pub struct QuizScore {
337 inner: quiz_core::QuizScore,
338}
339
340#[pymethods]
341impl QuizScore {
342 #[getter]
343 fn correct(&self) -> u32 {
344 self.inner.correct
345 }
346
347 #[getter]
348 fn wrong(&self) -> u32 {
349 self.inner.wrong
350 }
351
352 #[getter]
353 fn skipped(&self) -> u32 {
354 self.inner.skipped
355 }
356
357 #[getter]
358 fn total(&self) -> u32 {
359 self.inner.total
360 }
361
362 fn percentage(&self) -> f64 {
363 self.inner.percentage()
364 }
365
366 #[getter]
367 fn by_category(&self) -> Vec<CategoryScore> {
368 self.inner
369 .by_category
370 .iter()
371 .map(|s| CategoryScore { inner: s.clone() })
372 .collect()
373 }
374
375 #[getter]
376 fn by_difficulty(&self) -> Vec<DifficultyScore> {
377 self.inner
378 .by_difficulty
379 .iter()
380 .map(|s| DifficultyScore { inner: s.clone() })
381 .collect()
382 }
383
384 fn __repr__(&self) -> String {
385 format!(
386 "QuizScore({}/{} correct, {:.0}%)",
387 self.inner.correct,
388 self.inner.total,
389 self.inner.percentage()
390 )
391 }
392}
393
394#[pyclass]
399#[derive(Clone)]
400pub struct QuizConfig {
401 inner: quiz_core::QuizConfig,
402}
403
404#[pymethods]
405impl QuizConfig {
406 #[new]
407 #[pyo3(signature = (categories=vec![], difficulty=None, num_questions=10, shuffle=true, seed=None))]
408 fn new(
409 categories: Vec<Category>,
410 difficulty: Option<Difficulty>,
411 num_questions: u32,
412 shuffle: bool,
413 seed: Option<u64>,
414 ) -> Self {
415 Self {
416 inner: quiz_core::QuizConfig {
417 categories: categories.into_iter().map(|c| c.to_core()).collect(),
418 difficulty: difficulty.map(|d| d.to_core()),
419 num_questions,
420 shuffle,
421 seed,
422 },
423 }
424 }
425
426 fn __repr__(&self) -> String {
427 format!(
428 "QuizConfig(categories={}, difficulty={:?}, num_questions={})",
429 self.inner.categories.len(),
430 self.inner
431 .difficulty
432 .as_ref()
433 .map(|d| d.label())
434 .unwrap_or("All"),
435 self.inner.num_questions
436 )
437 }
438}
439
440#[pyclass]
445pub struct QuizSession {
446 inner: quiz_core::QuizSession,
447}
448
449#[pymethods]
450impl QuizSession {
451 #[new]
452 #[pyo3(signature = (config=None))]
453 fn new(config: Option<QuizConfig>) -> Self {
454 let config = config.map(|c| c.inner).unwrap_or_default();
455 Self {
456 inner: quiz_core::QuizSession::new(config),
457 }
458 }
459
460 fn current_question(&self) -> Option<Question> {
462 self.inner.current_question().map(|q| Question { inner: q })
463 }
464
465 fn answer(&mut self, choice: u8) -> AnswerResult {
467 AnswerResult {
468 inner: self.inner.answer(choice),
469 }
470 }
471
472 fn skip(&mut self) -> bool {
474 self.inner.skip()
475 }
476
477 fn is_finished(&self) -> bool {
479 self.inner.is_finished()
480 }
481
482 fn progress(&self) -> (usize, usize) {
484 self.inner.progress()
485 }
486
487 fn score(&self) -> QuizScore {
489 QuizScore {
490 inner: self.inner.score(),
491 }
492 }
493
494 fn __repr__(&self) -> String {
495 let (idx, total) = self.inner.progress();
496 format!("QuizSession(question {}/{})", idx + 1, total)
497 }
498}
499
500#[pyclass]
505pub struct QuizBank;
506
507#[pymethods]
508impl QuizBank {
509 #[staticmethod]
511 fn all_questions() -> Vec<Question> {
512 quiz_core::QuizBank::all_questions()
513 .into_iter()
514 .map(|q| Question { inner: q })
515 .collect()
516 }
517
518 #[staticmethod]
520 fn categories() -> Vec<(Category, u32)> {
521 quiz_core::QuizBank::categories()
522 .into_iter()
523 .map(|(c, n)| (Category::from_core(c), n))
524 .collect()
525 }
526
527 #[staticmethod]
529 fn total_count() -> u32 {
530 quiz_core::QuizBank::total_count()
531 }
532}
533
534#[pyclass]
539#[derive(Clone)]
540pub struct QuestionLocale {
541 inner: quiz_core::i18n::QuestionLocale,
542}
543
544#[pymethods]
545impl QuestionLocale {
546 #[getter]
547 fn text(&self) -> &str {
548 &self.inner.text
549 }
550
551 #[getter]
552 fn options(&self) -> Vec<String> {
553 self.inner.options.clone()
554 }
555
556 #[getter]
557 fn explanation(&self) -> &str {
558 &self.inner.explanation
559 }
560
561 fn __repr__(&self) -> String {
562 let preview = if self.inner.text.len() > 50 {
563 format!("{}...", &self.inner.text[..50])
564 } else {
565 self.inner.text.clone()
566 };
567 format!("QuestionLocale(\"{}\")", preview)
568 }
569}
570
571#[pyclass]
572pub struct QuizLocale {
573 inner: quiz_core::i18n::QuizLocale,
574}
575
576#[pymethods]
577impl QuizLocale {
578 #[staticmethod]
581 fn for_code(code: &str) -> Self {
582 Self {
583 inner: quiz_core::i18n::QuizLocale::for_code(code),
584 }
585 }
586
587 fn question(&self, id: u32) -> Option<QuestionLocale> {
589 self.inner
590 .question(id)
591 .map(|q| QuestionLocale { inner: q.clone() })
592 }
593
594 fn category_label(&self, category: Category) -> String {
596 self.inner.category_label(&category.to_core()).to_string()
597 }
598
599 fn difficulty_label(&self, difficulty: Difficulty) -> String {
601 self.inner
602 .difficulty_label(&difficulty.to_core())
603 .to_string()
604 }
605
606 fn ui_title(&self) -> &str {
608 &self.inner.ui.title
609 }
610
611 fn ui_correct(&self) -> &str {
612 &self.inner.ui.correct
613 }
614
615 fn ui_wrong(&self) -> &str {
616 &self.inner.ui.wrong
617 }
618
619 fn ui_start_quiz(&self) -> &str {
620 &self.inner.ui.start_quiz
621 }
622
623 fn ui_next_question(&self) -> &str {
624 &self.inner.ui.next_question
625 }
626
627 fn ui_see_results(&self) -> &str {
628 &self.inner.ui.see_results
629 }
630
631 fn ui_skip(&self) -> &str {
632 &self.inner.ui.skip
633 }
634
635 fn ui_try_again(&self) -> &str {
636 &self.inner.ui.try_again_btn
637 }
638
639 fn __repr__(&self) -> String {
640 format!("QuizLocale(\"{}\")", self.inner.ui.title)
641 }
642}
643
644#[pymodule]
649fn eulumdat_quiz(m: &Bound<'_, PyModule>) -> PyResult<()> {
650 m.add_class::<Category>()?;
651 m.add_class::<Difficulty>()?;
652 m.add_class::<Question>()?;
653 m.add_class::<AnswerResult>()?;
654 m.add_class::<QuizConfig>()?;
655 m.add_class::<QuizSession>()?;
656 m.add_class::<QuizBank>()?;
657 m.add_class::<QuizScore>()?;
658 m.add_class::<CategoryScore>()?;
659 m.add_class::<DifficultyScore>()?;
660 m.add_class::<QuizLocale>()?;
661 m.add_class::<QuestionLocale>()?;
662 Ok(())
663}