1use serde::{Deserialize, Serialize};
4use std::error::Error;
5use std::fmt;
6
7#[derive(Debug)]
9pub struct SessionBuildError(String);
10
11impl fmt::Display for SessionBuildError {
12 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13 write!(f, "Session build error: {}", self.0)
14 }
15}
16
17impl Error for SessionBuildError {}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct GithubRepoContext {
23 #[serde(skip_serializing_if = "Option::is_none")]
24 starting_branch: Option<String>,
25}
26
27impl GithubRepoContext {
28 #[must_use]
30 pub fn new(starting_branch: impl Into<String>) -> Self {
31 Self {
32 starting_branch: Some(starting_branch.into()),
33 }
34 }
35
36 #[must_use]
38 pub fn starting_branch(&self) -> Option<&str> {
39 self.starting_branch.as_deref()
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
45#[serde(rename_all = "camelCase")]
46pub struct SourceContext {
47 #[serde(skip_serializing_if = "Option::is_none")]
48 source: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 github_repo_context: Option<GithubRepoContext>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 environment_variables_enabled: Option<bool>,
53}
54
55impl SourceContext {
56 #[must_use]
58 pub fn new(source: impl Into<String>) -> Self {
59 Self {
60 source: Some(source.into()),
61 ..Self::default()
62 }
63 }
64
65 #[must_use]
67 pub fn with_github_repo_context(mut self, context: GithubRepoContext) -> Self {
68 self.github_repo_context = Some(context);
69 self
70 }
71
72 #[must_use]
74 pub fn with_environment_variables_enabled(mut self, enabled: bool) -> Self {
75 self.environment_variables_enabled = Some(enabled);
76 self
77 }
78
79 #[must_use]
81 pub fn source(&self) -> Option<&str> {
82 self.source.as_deref()
83 }
84
85 #[must_use]
87 pub fn github_repo_context(&self) -> Option<&GithubRepoContext> {
88 self.github_repo_context.as_ref()
89 }
90
91 #[must_use]
93 pub fn environment_variables_enabled(&self) -> Option<bool> {
94 self.environment_variables_enabled
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct Session {
105 #[serde(skip_serializing_if = "Option::is_none")]
106 id: Option<String>,
107 #[serde(skip_serializing_if = "Option::is_none")]
108 name: Option<String>,
109 #[serde(skip_serializing_if = "Option::is_none")]
110 title: Option<String>,
111 #[serde(skip_serializing_if = "Option::is_none")]
112 create_time: Option<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
114 update_time: Option<String>,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 state: Option<String>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 source_context: Option<SourceContext>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 prompt: Option<String>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 url: Option<String>,
123}
124
125impl Session {
126 #[must_use]
128 pub fn builder() -> SessionBuilder {
129 SessionBuilder::default()
130 }
131
132 #[must_use]
134 pub fn name(&self) -> Option<&str> {
135 self.name.as_deref()
136 }
137
138 #[must_use]
140 pub fn id(&self) -> Option<&str> {
141 self.id.as_deref()
142 }
143
144 #[must_use]
146 pub fn title(&self) -> Option<&str> {
147 self.title.as_deref()
148 }
149
150 #[must_use]
152 pub fn create_time(&self) -> Option<&str> {
153 self.create_time.as_deref()
154 }
155
156 #[must_use]
158 pub fn update_time(&self) -> Option<&str> {
159 self.update_time.as_deref()
160 }
161
162 #[must_use]
164 pub fn state(&self) -> Option<&str> {
165 self.state.as_deref()
166 }
167
168 #[must_use]
170 pub fn source_context(&self) -> Option<&SourceContext> {
171 self.source_context.as_ref()
172 }
173
174 #[must_use]
176 pub fn prompt(&self) -> Option<&str> {
177 self.prompt.as_deref()
178 }
179
180 #[must_use]
182 pub fn url(&self) -> Option<&str> {
183 self.url.as_deref()
184 }
185}
186
187#[derive(Debug, Default)]
189pub struct SessionBuilder {
190 id: Option<String>,
191 name: Option<String>,
192 title: Option<String>,
193 create_time: Option<String>,
194 update_time: Option<String>,
195 state: Option<String>,
196 source_context: Option<SourceContext>,
197 prompt: Option<String>,
198 url: Option<String>,
199}
200
201impl SessionBuilder {
202 #[must_use]
204 pub fn id(mut self, id: impl Into<String>) -> Self {
205 self.id = Some(id.into());
206 self
207 }
208
209 #[must_use]
211 pub fn name(mut self, name: impl Into<String>) -> Self {
212 self.name = Some(name.into());
213 self
214 }
215
216 #[must_use]
218 pub fn title(mut self, title: impl Into<String>) -> Self {
219 self.title = Some(title.into());
220 self
221 }
222
223 #[must_use]
225 pub fn create_time(mut self, create_time: impl Into<String>) -> Self {
226 self.create_time = Some(create_time.into());
227 self
228 }
229
230 #[must_use]
232 pub fn update_time(mut self, update_time: impl Into<String>) -> Self {
233 self.update_time = Some(update_time.into());
234 self
235 }
236
237 #[must_use]
239 pub fn state(mut self, state: impl Into<String>) -> Self {
240 self.state = Some(state.into());
241 self
242 }
243
244 #[must_use]
246 pub fn source_context(mut self, source_context: SourceContext) -> Self {
247 self.source_context = Some(source_context);
248 self
249 }
250
251 #[must_use]
253 pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
254 self.prompt = Some(prompt.into());
255 self
256 }
257
258 #[must_use]
260 pub fn url(mut self, url: impl Into<String>) -> Self {
261 self.url = Some(url.into());
262 self
263 }
264
265 pub fn build(self) -> Result<Session, SessionBuildError> {
271 Ok(Session {
272 id: self.id,
273 name: self.name,
274 title: self.title,
275 create_time: self.create_time,
276 update_time: self.update_time,
277 state: self.state,
278 source_context: self.source_context,
279 prompt: self.prompt,
280 url: self.url,
281 })
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn test_session_builder_with_name_and_id() {
291 let session = Session::builder()
292 .id("session-123")
293 .name("My Session")
294 .build()
295 .unwrap();
296
297 assert_eq!(session.id(), Some("session-123"));
298 assert_eq!(session.name(), Some("My Session"));
299 }
300
301 #[test]
302 fn test_session_builder_without_fields() {
303 let session = Session::builder().build().unwrap();
304
305 assert_eq!(session.id(), None);
306 assert_eq!(session.name(), None);
307 }
308
309 #[test]
310 fn test_session_builder_with_full_fields() {
311 let session = Session::builder()
312 .id("11413719004378428992")
313 .name("sessions/11413719004378428992")
314 .title("Example session")
315 .create_time("2026-08-08T12:42:12.441608052Z")
316 .update_time("2026-08-08T12:59:20.277897Z")
317 .state("AWAITING_USER_FEEDBACK")
318 .source_context(
319 SourceContext::new("sources/github/example-owner/example-repo")
320 .with_github_repo_context(GithubRepoContext::new("main"))
321 .with_environment_variables_enabled(true),
322 )
323 .prompt("Do the thing")
324 .url("https://jules.google.com/session/123")
325 .build()
326 .unwrap();
327
328 assert_eq!(session.title(), Some("Example session"));
329 assert_eq!(session.state(), Some("AWAITING_USER_FEEDBACK"));
330 assert_eq!(
331 session.source_context().and_then(SourceContext::source),
332 Some("sources/github/example-owner/example-repo")
333 );
334 assert_eq!(
335 session
336 .source_context()
337 .and_then(SourceContext::github_repo_context)
338 .and_then(GithubRepoContext::starting_branch),
339 Some("main")
340 );
341 }
342
343 #[test]
346 fn test_session_deserializes_real_api_shape() {
347 let json = r#"{
348 "name": "sessions/11413719004378428992",
349 "title": "Example session",
350 "createTime": "2026-08-08T12:42:12.441608052Z",
351 "updateTime": "2026-08-08T12:59:20.277897Z",
352 "state": "AWAITING_USER_FEEDBACK",
353 "sourceContext": {
354 "source": "sources/github/example-owner/example-repo",
355 "githubRepoContext": {
356 "startingBranch": "main"
357 },
358 "environmentVariablesEnabled": true
359 },
360 "prompt": "Do the thing",
361 "url": "https://jules.google.com/session/123",
362 "id": "11413719004378428992"
363 }"#;
364
365 let session: Session = serde_json::from_str(json).unwrap();
366 assert_eq!(session.name(), Some("sessions/11413719004378428992"));
367 assert_eq!(session.state(), Some("AWAITING_USER_FEEDBACK"));
368 assert_eq!(
369 session
370 .source_context()
371 .and_then(SourceContext::github_repo_context)
372 .and_then(GithubRepoContext::starting_branch),
373 Some("main")
374 );
375 }
376}