agentics_domain/models/
github.rs1use std::borrow::Cow;
4use std::fmt;
5
6use nutype::nutype;
7use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9
10pub const GITHUB_PULL_REQUEST_NUMBER_ERROR_MESSAGE: &str = "pr_number must be a positive integer";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct GithubPullRequestNumberError;
16
17impl fmt::Display for GithubPullRequestNumberError {
18 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 f.write_str(GITHUB_PULL_REQUEST_NUMBER_ERROR_MESSAGE)
21 }
22}
23
24impl std::error::Error for GithubPullRequestNumberError {}
25
26#[nutype(
27 sanitize(trim),
28 validate(
29 with = validate_github_pull_request_number,
30 error = GithubPullRequestNumberError
31 ),
32 derive(
33 Debug,
34 Clone,
35 PartialEq,
36 Eq,
37 PartialOrd,
38 Ord,
39 Hash,
40 AsRef,
41 Deref,
42 Display,
43 FromStr,
44 TryFrom,
45 ),
46)]
47pub struct GithubPullRequestNumber(String);
49
50impl GithubPullRequestNumber {
51 pub fn as_str(&self) -> &str {
53 self.as_ref()
54 }
55
56 pub fn as_i32(&self) -> Result<i32, GithubPullRequestNumberError> {
58 self.as_str()
59 .parse::<i32>()
60 .map_err(|_| GithubPullRequestNumberError)
61 }
62}
63
64impl Serialize for GithubPullRequestNumber {
65 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
67 where
68 S: Serializer,
69 {
70 let value = self.as_i32().map_err(serde::ser::Error::custom)?;
71 serializer.serialize_i32(value)
72 }
73}
74
75impl<'de> Deserialize<'de> for GithubPullRequestNumber {
76 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
78 where
79 D: Deserializer<'de>,
80 {
81 let value = i64::deserialize(deserializer)?;
82 let value = i32::try_from(value).map_err(serde::de::Error::custom)?;
83 Self::try_new(value.to_string()).map_err(serde::de::Error::custom)
84 }
85}
86
87impl JsonSchema for GithubPullRequestNumber {
88 fn inline_schema() -> bool {
90 true
91 }
92
93 fn schema_name() -> Cow<'static, str> {
95 "GithubPullRequestNumber".into()
96 }
97
98 fn json_schema(_: &mut SchemaGenerator) -> Schema {
100 json_schema!({
101 "type": "integer",
102 "minimum": 1,
103 "maximum": i32::MAX
104 })
105 }
106}
107
108fn validate_github_pull_request_number(value: &str) -> Result<(), GithubPullRequestNumberError> {
110 if value.is_empty() || value.starts_with('+') || value.starts_with('0') && value != "0" {
111 return Err(GithubPullRequestNumberError);
112 }
113 let Ok(number) = value.parse::<i32>() else {
114 return Err(GithubPullRequestNumberError);
115 };
116 if number > 0 {
117 Ok(())
118 } else {
119 Err(GithubPullRequestNumberError)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::GithubPullRequestNumber;
126
127 #[test]
129 fn validates_pull_request_numbers() {
130 let number = GithubPullRequestNumber::try_new(" 42 ".to_string())
131 .expect("positive PR number should parse");
132 assert_eq!(number.as_str(), "42");
133 assert_eq!(number.as_i32().expect("number should fit i32"), 42);
134 for value in ["", "0", "-1", "42abc", "42.9", "01"] {
135 assert!(GithubPullRequestNumber::try_new(value.to_string()).is_err());
136 }
137 }
138
139 #[test]
141 fn serde_uses_numeric_json() {
142 let number: GithubPullRequestNumber =
143 serde_json::from_str("42").expect("numeric PR number should deserialize");
144 assert_eq!(number.as_str(), "42");
145 assert_eq!(
146 serde_json::to_string(&number).expect("PR number should serialize"),
147 "42"
148 );
149 assert!(serde_json::from_str::<GithubPullRequestNumber>("\"42\"").is_err());
150 }
151}