wowlab_common/sim/
job_meta.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, thiserror::Error)]
7#[non_exhaustive]
8#[error("JSON parse error: {source}")]
9pub struct JobMetaError {
10 #[source]
11 source: serde_json::Error,
12}
13
14impl JobMetaError {
15 fn parse(source: serde_json::Error) -> Self {
16 Self { source }
17 }
18}
19
20impl From<serde_json::Error> for JobMetaError {
21 fn from(source: serde_json::Error) -> Self {
22 Self::parse(source)
23 }
24}
25
26#[derive(Clone, Debug, Deserialize, Serialize)]
28#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
29#[cfg_attr(feature = "wasm", tsify(into_wasm_abi))]
30pub struct JobMeta {
31 pub status: String,
32 pub created_at: String,
33 pub completed_at: String,
34 pub pinned: bool,
35 pub spec_id: i32,
36 pub season_id: String,
37}
38
39impl JobMeta {
40 #[must_use]
41 pub fn new_pending() -> Self {
42 Self {
43 status: "pending".to_string(),
44 created_at: String::new(),
45 completed_at: String::new(),
46 pinned: false,
47 spec_id: 0,
48 season_id: String::new(),
49 }
50 }
51}
52
53#[must_use]
59pub fn build_job_meta(meta: &JobMeta) -> String {
60 serde_json::to_string(meta).expect("JobMeta serialization should never fail")
61}
62
63pub fn parse_job_meta(json: &str) -> Result<JobMeta, JobMetaError> {
69 Ok(serde_json::from_str(json)?)
70}
71
72#[cfg(test)]
73mod tests {
74 use std::error::Error as _;
75
76 use googletest::prelude::*;
77
78 use super::*;
79
80 #[gtest]
81 fn test_roundtrip() -> Result<()> {
82 let meta = JobMeta {
83 status: "completed".into(),
84 created_at: "2026-03-04T00:00:00Z".into(),
85 completed_at: "2026-03-04T00:01:00Z".into(),
86 pinned: true,
87 spec_id: 253,
88 season_id: "tww-s2".into(),
89 };
90 let json = build_job_meta(&meta);
91 let parsed = parse_job_meta(&json).or_fail()?;
92
93 verify_that!(
94 parsed,
95 matches_pattern!(JobMeta {
96 status: "completed",
97 spec_id: eq(&253),
98 pinned: eq(&true),
99 season_id: "tww-s2",
100 ..
101 })
102 )
103 }
104
105 #[gtest]
106 fn test_new_pending() -> Result<()> {
107 let meta = JobMeta::new_pending();
108
109 verify_that!(
110 meta,
111 matches_pattern!(JobMeta {
112 status: "pending",
113 pinned: eq(&false),
114 spec_id: eq(&0),
115 season_id: "",
116 ..
117 })
118 )
119 }
120
121 #[gtest]
122 fn malformed_empty_and_missing_json_preserve_parse_sources() -> Result<()> {
123 for json in ["{", "", r#"{"status":"pending"}"#] {
124 let error = parse_job_meta(json).err().or_fail()?;
125
126 verify_that!(error.to_string(), starts_with("JSON parse error:"))?;
127 verify_true!(
128 error
129 .source()
130 .is_some_and(<dyn std::error::Error>::is::<serde_json::Error>)
131 )?;
132 }
133
134 Ok(())
135 }
136
137 #[gtest]
138 fn unicode_metadata_roundtrips_unchanged() -> Result<()> {
139 let meta = JobMeta {
140 status: "terminé-世界".to_string(),
141 created_at: "époque".to_string(),
142 completed_at: "🦊".to_string(),
143 pinned: false,
144 spec_id: 253,
145 season_id: "saison-été".to_string(),
146 };
147
148 let parsed = parse_job_meta(&build_job_meta(&meta)).or_fail()?;
149
150 verify_that!(
151 parsed,
152 matches_pattern!(JobMeta {
153 status: eq(&meta.status),
154 created_at: eq(&meta.created_at),
155 completed_at: eq(&meta.completed_at),
156 season_id: eq(&meta.season_id),
157 ..
158 })
159 )
160 }
161}