Skip to content

Commit 0fe6d4f

Browse files
committed
feat(schema): impl deserialize an instance of type T from a string of YAML text
1 parent 3e0a30a commit 0fe6d4f

File tree

6 files changed

+208
-6
lines changed

6 files changed

+208
-6
lines changed

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dotenv = "0.15.0"
2828
indexmap = {version = "2.9.0", features = ["serde"] }
2929
serde = { version = "1.0.219", features = ["derive"] }
3030
serde_json = "1.0.140"
31+
serde_yml = "0.0.12"
3132
thiserror = "2.0.12"
3233
tokio = { version = "1.45.1", features = ["full"] }
3334
tracing = "0.1.41"

src/schema/course.rs

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use std::str::FromStr;
16+
1517
use indexmap::IndexSet;
1618
use serde::{Deserialize, Serialize};
1719

18-
use crate::schema::{Extension, Stage};
20+
use crate::schema::{Extensions, Stage};
1921

2022
/// Schema for the course.yml file.
2123
#[derive(Clone, Debug, Deserialize, Serialize)]
@@ -39,11 +41,20 @@ pub struct Course {
3941
pub summary: String,
4042

4143
/// Sequential stages of the course.
44+
#[serde(skip)]
4245
pub stages: IndexSet<Stage>,
4346

4447
/// Sets of additional stages.
45-
#[serde(skip_serializing_if = "Option::is_none")]
46-
pub extensions: Option<IndexSet<Extension>>,
48+
#[serde(skip)]
49+
pub extensions: Option<Extensions>,
50+
}
51+
52+
impl FromStr for Course {
53+
type Err = String;
54+
55+
fn from_str(s: &str) -> Result<Self, Self::Err> {
56+
serde_yml::from_str(s).map_err(|e| e.to_string())
57+
}
4758
}
4859

4960
/// The release status of the course.
@@ -53,3 +64,36 @@ pub enum Status {
5364
Beta,
5465
Live,
5566
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
72+
#[test]
73+
fn test_course_from_str() {
74+
let yaml = r#"
75+
slug: rust-course
76+
name: Rust Programming
77+
short_name: Rust
78+
release_status: Beta
79+
description: A comprehensive course on Rust programming language.
80+
summary: Learn Rust programming
81+
"#;
82+
83+
let course = Course::from_str(yaml).unwrap();
84+
85+
assert_eq!(course.slug, "rust-course");
86+
assert_eq!(course.name, "Rust Programming");
87+
assert_eq!(course.short_name, "Rust");
88+
assert!(matches!(course.release_status, Status::Beta));
89+
assert_eq!(course.description, "A comprehensive course on Rust programming language.");
90+
assert_eq!(course.summary, "Learn Rust programming");
91+
}
92+
93+
#[test]
94+
fn test_course_from_str_error() {
95+
let invalid_yaml = "invalid: yaml: content";
96+
let result = Course::from_str(invalid_yaml);
97+
assert!(result.is_err());
98+
}
99+
}

src/schema/extension.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,31 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
use std::hash::Hash;
15+
use std::{hash::Hash, str::FromStr};
1616

1717
use indexmap::IndexSet;
1818
use serde::{Deserialize, Serialize};
1919

2020
use crate::schema::Stage;
2121

22+
/// Schema for the extensions.yml file.
23+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
24+
pub struct Extensions(IndexSet<Extension>);
25+
26+
impl Extensions {
27+
pub fn iter(&self) -> indexmap::set::Iter<Extension> {
28+
self.0.iter()
29+
}
30+
}
31+
32+
impl FromStr for Extensions {
33+
type Err = String;
34+
35+
fn from_str(s: &str) -> Result<Self, Self::Err> {
36+
serde_yml::from_str(s).map_err(|e| e.to_string())
37+
}
38+
}
39+
2240
/// Schema for the extension.
2341
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
2442
pub struct Extension {
@@ -32,6 +50,7 @@ pub struct Extension {
3250
pub description: String,
3351

3452
/// Sequential stages of the extension.
53+
#[serde(skip)]
3554
pub stages: IndexSet<Stage>,
3655
}
3756

@@ -40,3 +59,40 @@ impl Hash for Extension {
4059
self.slug.hash(state);
4160
}
4261
}
62+
63+
#[cfg(test)]
64+
mod tests {
65+
use super::*;
66+
67+
#[test]
68+
fn test_extensions_from_str() {
69+
let yaml = r#"
70+
- slug: test1
71+
name: Test Extension 1
72+
description: Test description 1
73+
- slug: test2
74+
name: Test Extension 2
75+
description: Test description 2
76+
"#;
77+
78+
let extensions = Extensions::from_str(yaml).unwrap();
79+
80+
let mut iter = extensions.iter();
81+
82+
let first = iter.next().unwrap();
83+
assert_eq!(first.slug, "test1");
84+
assert_eq!(first.name, "Test Extension 1");
85+
86+
let second = iter.next().unwrap();
87+
assert_eq!(second.slug, "test2");
88+
assert_eq!(second.name, "Test Extension 2");
89+
assert!(iter.next().is_none());
90+
}
91+
92+
#[test]
93+
fn test_extensions_from_str_invalid() {
94+
let invalid_yaml = "invalid: yaml";
95+
let result = Extensions::from_str(invalid_yaml);
96+
assert!(result.is_err());
97+
}
98+
}

src/schema/manifest.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
use std::str::FromStr;
16+
1517
use serde::{Deserialize, Serialize};
1618

1719
/// Schema for the codecraft.yml file.
@@ -29,3 +31,39 @@ pub struct Manifest {
2931
/// The main source file that users can edit for this project.
3032
pub user_editable_file: String,
3133
}
34+
35+
impl FromStr for Manifest {
36+
type Err = String;
37+
38+
fn from_str(s: &str) -> Result<Self, Self::Err> {
39+
serde_yml::from_str(s).map_err(|e| e.to_string())
40+
}
41+
}
42+
43+
#[cfg(test)]
44+
mod tests {
45+
use super::*;
46+
47+
#[test]
48+
fn test_manifest_from_str() {
49+
let yaml = r#"
50+
debug: true
51+
language_pack: "rust-1.70.0"
52+
required_executable: "cargo"
53+
user_editable_file: "src/main.rs"
54+
"#;
55+
56+
let manifest = Manifest::from_str(yaml).unwrap();
57+
assert!(manifest.debug);
58+
assert_eq!(manifest.language_pack, "rust-1.70.0");
59+
assert_eq!(manifest.required_executable, "cargo");
60+
assert_eq!(manifest.user_editable_file, "src/main.rs");
61+
}
62+
63+
#[test]
64+
fn test_manifest_from_str_error() {
65+
let invalid_yaml = "invalid: yaml: content";
66+
let result = Manifest::from_str(invalid_yaml);
67+
assert!(result.is_err());
68+
}
69+
}

src/schema/stage.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414

1515
use serde::{Deserialize, Serialize};
16-
use std::hash::Hash;
16+
use std::{hash::Hash, str::FromStr};
1717

1818
/// A self-contained coding task with specific objectives and validation.
1919
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
@@ -34,10 +34,11 @@ pub struct Stage {
3434
pub description: String,
3535

3636
/// A markdown description for this stage.
37+
#[serde(skip)]
3738
pub instruction: String,
3839

3940
/// The solution to this stage, if available.
40-
#[serde(skip_serializing_if = "Option::is_none")]
41+
#[serde(skip)]
4142
pub solution: Option<Solution>,
4243
}
4344

@@ -47,6 +48,14 @@ impl Hash for Stage {
4748
}
4849
}
4950

51+
impl FromStr for Stage {
52+
type Err = String;
53+
54+
fn from_str(s: &str) -> Result<Self, Self::Err> {
55+
serde_yml::from_str(s).map_err(|e| e.to_string())
56+
}
57+
}
58+
5059
/// A difficulty rating,
5160
/// from the perspective of a proficient programmer.
5261
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
@@ -80,3 +89,31 @@ impl Solution {
8089
self.patches.push((path, content));
8190
}
8291
}
92+
93+
#[cfg(test)]
94+
mod tests {
95+
use super::*;
96+
97+
#[test]
98+
fn test_stage_from_str() {
99+
let yaml = r#"
100+
slug: test-stage
101+
name: Test Stage
102+
difficulty: Easy
103+
description: A test stage
104+
"#;
105+
106+
let stage = Stage::from_str(yaml).unwrap();
107+
assert_eq!(stage.slug, "test-stage");
108+
assert_eq!(stage.name, "Test Stage");
109+
assert_eq!(stage.difficulty, Difficulty::Easy);
110+
assert_eq!(stage.description, "A test stage");
111+
}
112+
113+
#[test]
114+
fn test_stage_from_str_invalid() {
115+
let invalid_yaml = "invalid: yaml: content";
116+
let result = Stage::from_str(invalid_yaml);
117+
assert!(result.is_err());
118+
}
119+
}

0 commit comments

Comments
 (0)