1use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "backend")]
12pub mod crypto;
13#[cfg(feature = "backend")]
14pub mod db;
15pub mod deploy;
16pub mod oauth;
17#[cfg(feature = "backend")]
18pub mod service;
19
20pub use opensession_core::trace::{
22 Agent, Content, ContentBlock, Event, EventType, Session, SessionContext, Stats,
23};
24
25#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
29#[serde(rename_all = "snake_case")]
30#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
31#[cfg_attr(feature = "ts", ts(export))]
32pub enum SortOrder {
33 #[default]
34 Recent,
35 Popular,
36 Longest,
37}
38
39impl SortOrder {
40 pub fn as_str(&self) -> &str {
41 match self {
42 Self::Recent => "recent",
43 Self::Popular => "popular",
44 Self::Longest => "longest",
45 }
46 }
47}
48
49impl std::fmt::Display for SortOrder {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.write_str(self.as_str())
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
57#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
58#[cfg_attr(feature = "ts", ts(export))]
59pub enum TimeRange {
60 #[serde(rename = "24h")]
61 Hours24,
62 #[serde(rename = "7d")]
63 Days7,
64 #[serde(rename = "30d")]
65 Days30,
66 #[default]
67 #[serde(rename = "all")]
68 All,
69}
70
71impl TimeRange {
72 pub fn as_str(&self) -> &str {
73 match self {
74 Self::Hours24 => "24h",
75 Self::Days7 => "7d",
76 Self::Days30 => "30d",
77 Self::All => "all",
78 }
79 }
80}
81
82impl std::fmt::Display for TimeRange {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 f.write_str(self.as_str())
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
90#[serde(rename_all = "snake_case")]
91#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
92#[cfg_attr(feature = "ts", ts(export))]
93pub enum LinkType {
94 Handoff,
95 Related,
96 Parent,
97 Child,
98}
99
100impl LinkType {
101 pub fn as_str(&self) -> &str {
102 match self {
103 Self::Handoff => "handoff",
104 Self::Related => "related",
105 Self::Parent => "parent",
106 Self::Child => "child",
107 }
108 }
109}
110
111impl std::fmt::Display for LinkType {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.write_str(self.as_str())
114 }
115}
116
117pub fn saturating_i64(v: u64) -> i64 {
121 i64::try_from(v).unwrap_or(i64::MAX)
122}
123
124#[derive(Debug, Serialize, Deserialize)]
128#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
129#[cfg_attr(feature = "ts", ts(export))]
130pub struct AuthRegisterRequest {
131 pub email: String,
132 pub password: String,
133 pub nickname: String,
134}
135
136#[derive(Debug, Serialize, Deserialize)]
138#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
139#[cfg_attr(feature = "ts", ts(export))]
140pub struct LoginRequest {
141 pub email: String,
142 pub password: String,
143}
144
145#[derive(Debug, Serialize, Deserialize)]
147#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
148#[cfg_attr(feature = "ts", ts(export))]
149pub struct AuthTokenResponse {
150 pub access_token: String,
151 pub refresh_token: String,
152 pub expires_in: u64,
153 pub user_id: String,
154 pub nickname: String,
155}
156
157#[derive(Debug, Serialize, Deserialize)]
159#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
160#[cfg_attr(feature = "ts", ts(export))]
161pub struct RefreshRequest {
162 pub refresh_token: String,
163}
164
165#[derive(Debug, Serialize, Deserialize)]
167#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
168#[cfg_attr(feature = "ts", ts(export))]
169pub struct LogoutRequest {
170 pub refresh_token: String,
171}
172
173#[derive(Debug, Serialize, Deserialize)]
175#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
176#[cfg_attr(feature = "ts", ts(export))]
177pub struct ChangePasswordRequest {
178 pub current_password: String,
179 pub new_password: String,
180}
181
182#[derive(Debug, Serialize, Deserialize)]
184#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
185#[cfg_attr(feature = "ts", ts(export))]
186pub struct VerifyResponse {
187 pub user_id: String,
188 pub nickname: String,
189}
190
191#[derive(Debug, Serialize, Deserialize)]
193#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
194#[cfg_attr(feature = "ts", ts(export))]
195pub struct UserSettingsResponse {
196 pub user_id: String,
197 pub nickname: String,
198 pub created_at: String,
199 pub email: Option<String>,
200 pub avatar_url: Option<String>,
201 #[serde(default)]
203 pub oauth_providers: Vec<oauth::LinkedProvider>,
204}
205
206#[derive(Debug, Serialize, Deserialize)]
208#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
209#[cfg_attr(feature = "ts", ts(export))]
210pub struct OkResponse {
211 pub ok: bool,
212}
213
214#[derive(Debug, Serialize, Deserialize)]
216#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
217#[cfg_attr(feature = "ts", ts(export))]
218pub struct IssueApiKeyResponse {
219 pub api_key: String,
220}
221
222#[derive(Debug, Serialize)]
224#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
225#[cfg_attr(feature = "ts", ts(export))]
226pub struct OAuthLinkResponse {
227 pub url: String,
228}
229
230#[derive(Debug, Serialize, Deserialize)]
234pub struct UploadRequest {
235 pub session: Session,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub body_url: Option<String>,
238 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub linked_session_ids: Option<Vec<String>>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
241 pub git_remote: Option<String>,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
243 pub git_branch: Option<String>,
244 #[serde(default, skip_serializing_if = "Option::is_none")]
245 pub git_commit: Option<String>,
246 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub git_repo_name: Option<String>,
248 #[serde(default, skip_serializing_if = "Option::is_none")]
249 pub pr_number: Option<i64>,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
251 pub pr_url: Option<String>,
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 pub score_plugin: Option<String>,
254}
255
256#[derive(Debug, Serialize, Deserialize)]
258#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
259#[cfg_attr(feature = "ts", ts(export))]
260pub struct UploadResponse {
261 pub id: String,
262 pub url: String,
263 #[serde(default)]
264 pub session_score: i64,
265 #[serde(default = "default_score_plugin")]
266 pub score_plugin: String,
267}
268
269#[derive(Debug, Clone, Serialize, Deserialize)]
272#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
273#[cfg_attr(feature = "ts", ts(export))]
274pub struct SessionSummary {
275 pub id: String,
276 pub user_id: Option<String>,
277 pub nickname: Option<String>,
278 pub tool: String,
279 pub agent_provider: Option<String>,
280 pub agent_model: Option<String>,
281 pub title: Option<String>,
282 pub description: Option<String>,
283 pub tags: Option<String>,
285 pub created_at: String,
286 pub uploaded_at: String,
287 pub message_count: i64,
288 pub task_count: i64,
289 pub event_count: i64,
290 pub duration_seconds: i64,
291 pub total_input_tokens: i64,
292 pub total_output_tokens: i64,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub git_remote: Option<String>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub git_branch: Option<String>,
297 #[serde(default, skip_serializing_if = "Option::is_none")]
298 pub git_commit: Option<String>,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub git_repo_name: Option<String>,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub pr_number: Option<i64>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub pr_url: Option<String>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub working_directory: Option<String>,
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub files_modified: Option<String>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub files_read: Option<String>,
311 #[serde(default)]
312 pub has_errors: bool,
313 #[serde(default = "default_max_active_agents")]
314 pub max_active_agents: i64,
315 #[serde(default)]
316 pub session_score: i64,
317 #[serde(default = "default_score_plugin")]
318 pub score_plugin: String,
319}
320
321#[derive(Debug, Serialize, Deserialize)]
323#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
324#[cfg_attr(feature = "ts", ts(export))]
325pub struct SessionListResponse {
326 pub sessions: Vec<SessionSummary>,
327 pub total: i64,
328 pub page: u32,
329 pub per_page: u32,
330}
331
332#[derive(Debug, Deserialize)]
334#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
335#[cfg_attr(feature = "ts", ts(export))]
336pub struct SessionListQuery {
337 #[serde(default = "default_page")]
338 pub page: u32,
339 #[serde(default = "default_per_page")]
340 pub per_page: u32,
341 pub search: Option<String>,
342 pub tool: Option<String>,
343 pub sort: Option<SortOrder>,
345 pub time_range: Option<TimeRange>,
347}
348
349impl SessionListQuery {
350 pub fn is_public_feed_cacheable(
352 &self,
353 has_auth_header: bool,
354 has_session_cookie: bool,
355 ) -> bool {
356 !has_auth_header
357 && !has_session_cookie
358 && self.search.as_deref().is_none_or(|s| s.trim().is_empty())
359 && self.page <= 10
360 && self.per_page <= 50
361 }
362}
363
364#[cfg(test)]
365mod session_list_query_tests {
366 use super::*;
367
368 fn base_query() -> SessionListQuery {
369 SessionListQuery {
370 page: 1,
371 per_page: 20,
372 search: None,
373 tool: None,
374 sort: None,
375 time_range: None,
376 }
377 }
378
379 #[test]
380 fn public_feed_cacheable_when_anonymous_default_feed() {
381 let q = base_query();
382 assert!(q.is_public_feed_cacheable(false, false));
383 }
384
385 #[test]
386 fn public_feed_not_cacheable_with_auth_or_cookie() {
387 let q = base_query();
388 assert!(!q.is_public_feed_cacheable(true, false));
389 assert!(!q.is_public_feed_cacheable(false, true));
390 }
391
392 #[test]
393 fn public_feed_not_cacheable_for_search_or_large_page() {
394 let mut q = base_query();
395 q.search = Some("hello".into());
396 assert!(!q.is_public_feed_cacheable(false, false));
397
398 let mut q = base_query();
399 q.page = 11;
400 assert!(!q.is_public_feed_cacheable(false, false));
401
402 let mut q = base_query();
403 q.per_page = 100;
404 assert!(!q.is_public_feed_cacheable(false, false));
405 }
406}
407
408fn default_page() -> u32 {
409 1
410}
411fn default_per_page() -> u32 {
412 20
413}
414fn default_max_active_agents() -> i64 {
415 1
416}
417
418fn default_score_plugin() -> String {
419 opensession_core::scoring::DEFAULT_SCORE_PLUGIN.to_string()
420}
421
422#[derive(Debug, Serialize, Deserialize)]
424#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
425#[cfg_attr(feature = "ts", ts(export))]
426pub struct SessionDetail {
427 #[serde(flatten)]
428 #[cfg_attr(feature = "ts", ts(flatten))]
429 pub summary: SessionSummary,
430 #[serde(default, skip_serializing_if = "Vec::is_empty")]
431 pub linked_sessions: Vec<SessionLink>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize)]
436#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
437#[cfg_attr(feature = "ts", ts(export))]
438pub struct SessionLink {
439 pub session_id: String,
440 pub linked_session_id: String,
441 pub link_type: LinkType,
442 pub created_at: String,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447#[serde(tag = "kind", rename_all = "snake_case")]
448#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
449#[cfg_attr(feature = "ts", ts(export))]
450pub enum ParseSource {
451 Git {
453 remote: String,
454 r#ref: String,
455 path: String,
456 },
457 Github {
459 owner: String,
460 repo: String,
461 r#ref: String,
462 path: String,
463 },
464 Inline {
466 filename: String,
467 content_base64: String,
469 },
470}
471
472#[derive(Debug, Clone, Serialize, Deserialize)]
474#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
475#[cfg_attr(feature = "ts", ts(export))]
476pub struct ParseCandidate {
477 pub id: String,
478 pub confidence: u8,
479 pub reason: String,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
485#[cfg_attr(feature = "ts", ts(export))]
486pub struct ParsePreviewRequest {
487 pub source: ParseSource,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub parser_hint: Option<String>,
490}
491
492#[derive(Debug, Clone, Serialize, Deserialize)]
494#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
495#[cfg_attr(feature = "ts", ts(export))]
496pub struct ParsePreviewResponse {
497 pub parser_used: String,
498 #[serde(default)]
499 pub parser_candidates: Vec<ParseCandidate>,
500 #[cfg_attr(feature = "ts", ts(type = "any"))]
501 pub session: Session,
502 pub source: ParseSource,
503 #[serde(default)]
504 pub warnings: Vec<String>,
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub native_adapter: Option<String>,
507}
508
509#[derive(Debug, Clone, Serialize, Deserialize)]
511#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
512#[cfg_attr(feature = "ts", ts(export))]
513pub struct ParsePreviewErrorResponse {
514 pub code: String,
515 pub message: String,
516 #[serde(default, skip_serializing_if = "Vec::is_empty")]
517 pub parser_candidates: Vec<ParseCandidate>,
518}
519
520#[derive(Debug, Deserialize)]
524#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
525#[cfg_attr(feature = "ts", ts(export))]
526pub struct StreamEventsRequest {
527 #[cfg_attr(feature = "ts", ts(type = "any"))]
528 pub agent: Option<Agent>,
529 #[cfg_attr(feature = "ts", ts(type = "any"))]
530 pub context: Option<SessionContext>,
531 #[cfg_attr(feature = "ts", ts(type = "any[]"))]
532 pub events: Vec<Event>,
533}
534
535#[derive(Debug, Serialize, Deserialize)]
537#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
538#[cfg_attr(feature = "ts", ts(export))]
539pub struct StreamEventsResponse {
540 pub accepted: usize,
541}
542
543#[derive(Debug, Serialize, Deserialize)]
547#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
548#[cfg_attr(feature = "ts", ts(export))]
549pub struct HealthResponse {
550 pub status: String,
551 pub version: String,
552}
553
554#[derive(Debug, Serialize, Deserialize)]
556#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
557#[cfg_attr(feature = "ts", ts(export))]
558pub struct CapabilitiesResponse {
559 pub auth_enabled: bool,
560 pub parse_preview_enabled: bool,
561 pub register_targets: Vec<String>,
562 pub share_modes: Vec<String>,
563}
564
565pub const DEFAULT_REGISTER_TARGETS: &[&str] = &["local", "git"];
566pub const DEFAULT_SHARE_MODES: &[&str] = &["web", "git", "json"];
567
568impl CapabilitiesResponse {
569 pub fn for_runtime(auth_enabled: bool, parse_preview_enabled: bool) -> Self {
571 Self {
572 auth_enabled,
573 parse_preview_enabled,
574 register_targets: DEFAULT_REGISTER_TARGETS
575 .iter()
576 .map(|target| (*target).to_string())
577 .collect(),
578 share_modes: DEFAULT_SHARE_MODES
579 .iter()
580 .map(|mode| (*mode).to_string())
581 .collect(),
582 }
583 }
584}
585
586#[derive(Debug, Clone)]
593#[non_exhaustive]
594pub enum ServiceError {
595 BadRequest(String),
596 Unauthorized(String),
597 Forbidden(String),
598 NotFound(String),
599 Conflict(String),
600 Internal(String),
601}
602
603impl ServiceError {
604 pub fn status_code(&self) -> u16 {
606 match self {
607 Self::BadRequest(_) => 400,
608 Self::Unauthorized(_) => 401,
609 Self::Forbidden(_) => 403,
610 Self::NotFound(_) => 404,
611 Self::Conflict(_) => 409,
612 Self::Internal(_) => 500,
613 }
614 }
615
616 pub fn code(&self) -> &'static str {
618 match self {
619 Self::BadRequest(_) => "bad_request",
620 Self::Unauthorized(_) => "unauthorized",
621 Self::Forbidden(_) => "forbidden",
622 Self::NotFound(_) => "not_found",
623 Self::Conflict(_) => "conflict",
624 Self::Internal(_) => "internal",
625 }
626 }
627
628 pub fn message(&self) -> &str {
630 match self {
631 Self::BadRequest(m)
632 | Self::Unauthorized(m)
633 | Self::Forbidden(m)
634 | Self::NotFound(m)
635 | Self::Conflict(m)
636 | Self::Internal(m) => m,
637 }
638 }
639
640 pub fn from_db<E: std::fmt::Display>(context: &str) -> impl FnOnce(E) -> Self + '_ {
642 move |e| Self::Internal(format!("{context}: {e}"))
643 }
644}
645
646impl std::fmt::Display for ServiceError {
647 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
648 write!(f, "{}", self.message())
649 }
650}
651
652impl std::error::Error for ServiceError {}
653
654#[derive(Debug, Serialize)]
658#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
659#[cfg_attr(feature = "ts", ts(export))]
660pub struct ApiError {
661 pub code: String,
662 pub message: String,
663}
664
665impl From<&ServiceError> for ApiError {
666 fn from(e: &ServiceError) -> Self {
667 Self {
668 code: e.code().to_string(),
669 message: e.message().to_string(),
670 }
671 }
672}
673
674#[cfg(test)]
677mod schema_tests {
678 use super::*;
679
680 #[test]
681 fn parse_preview_request_round_trip_git() {
682 let req = ParsePreviewRequest {
683 source: ParseSource::Git {
684 remote: "https://github.com/hwisu/opensession".to_string(),
685 r#ref: "main".to_string(),
686 path: "sessions/demo.hail.jsonl".to_string(),
687 },
688 parser_hint: Some("hail".to_string()),
689 };
690
691 let json = serde_json::to_string(&req).expect("request should serialize");
692 let decoded: ParsePreviewRequest =
693 serde_json::from_str(&json).expect("request should deserialize");
694
695 match decoded.source {
696 ParseSource::Git {
697 remote,
698 r#ref,
699 path,
700 } => {
701 assert_eq!(remote, "https://github.com/hwisu/opensession");
702 assert_eq!(r#ref, "main");
703 assert_eq!(path, "sessions/demo.hail.jsonl");
704 }
705 _ => panic!("expected git parse source"),
706 }
707 assert_eq!(decoded.parser_hint.as_deref(), Some("hail"));
708 }
709
710 #[test]
711 fn parse_preview_request_round_trip_github_compat() {
712 let req = ParsePreviewRequest {
713 source: ParseSource::Github {
714 owner: "hwisu".to_string(),
715 repo: "opensession".to_string(),
716 r#ref: "main".to_string(),
717 path: "sessions/demo.hail.jsonl".to_string(),
718 },
719 parser_hint: Some("hail".to_string()),
720 };
721
722 let json = serde_json::to_string(&req).expect("request should serialize");
723 let decoded: ParsePreviewRequest =
724 serde_json::from_str(&json).expect("request should deserialize");
725
726 match decoded.source {
727 ParseSource::Github {
728 owner,
729 repo,
730 r#ref,
731 path,
732 } => {
733 assert_eq!(owner, "hwisu");
734 assert_eq!(repo, "opensession");
735 assert_eq!(r#ref, "main");
736 assert_eq!(path, "sessions/demo.hail.jsonl");
737 }
738 _ => panic!("expected github parse source"),
739 }
740 assert_eq!(decoded.parser_hint.as_deref(), Some("hail"));
741 }
742
743 #[test]
744 fn parse_preview_error_response_round_trip_with_candidates() {
745 let payload = ParsePreviewErrorResponse {
746 code: "parser_selection_required".to_string(),
747 message: "choose parser".to_string(),
748 parser_candidates: vec![ParseCandidate {
749 id: "codex".to_string(),
750 confidence: 89,
751 reason: "event markers".to_string(),
752 }],
753 };
754
755 let json = serde_json::to_string(&payload).expect("error payload should serialize");
756 let decoded: ParsePreviewErrorResponse =
757 serde_json::from_str(&json).expect("error payload should deserialize");
758
759 assert_eq!(decoded.code, "parser_selection_required");
760 assert_eq!(decoded.parser_candidates.len(), 1);
761 assert_eq!(decoded.parser_candidates[0].id, "codex");
762 }
763
764 #[test]
765 fn capabilities_response_round_trip_includes_new_fields() {
766 let caps = CapabilitiesResponse::for_runtime(true, true);
767
768 let json = serde_json::to_string(&caps).expect("capabilities should serialize");
769 let decoded: CapabilitiesResponse =
770 serde_json::from_str(&json).expect("capabilities should deserialize");
771
772 assert!(decoded.auth_enabled);
773 assert!(decoded.parse_preview_enabled);
774 assert_eq!(decoded.register_targets, vec!["local", "git"]);
775 assert_eq!(decoded.share_modes, vec!["web", "git", "json"]);
776 }
777
778 #[test]
779 fn capabilities_defaults_are_stable() {
780 assert_eq!(DEFAULT_REGISTER_TARGETS, &["local", "git"]);
781 assert_eq!(DEFAULT_SHARE_MODES, &["web", "git", "json"]);
782 }
783}
784
785#[cfg(all(test, feature = "ts"))]
786mod tests {
787 use super::*;
788 use std::io::Write;
789 use std::path::PathBuf;
790 use ts_rs::TS;
791
792 #[test]
794 fn export_typescript() {
795 let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
796 .join("../../packages/ui/src/api-types.generated.ts");
797
798 let cfg = ts_rs::Config::new().with_large_int("number");
799 let mut parts: Vec<String> = Vec::new();
800 parts.push("// AUTO-GENERATED by opensession-api — DO NOT EDIT".to_string());
801 parts.push(
802 "// Regenerate with: cargo test -p opensession-api -- export_typescript".to_string(),
803 );
804 parts.push(String::new());
805
806 macro_rules! collect_ts {
810 ($($t:ty),+ $(,)?) => {
811 $(
812 let decl = <$t>::decl(&cfg);
813 let is_struct_decl = decl.contains(" = {") && !decl.contains("} |");
814 let decl = if is_struct_decl {
815 decl
817 .replacen("type ", "export interface ", 1)
818 .replace(" = {", " {")
819 .trim_end_matches(';')
820 .to_string()
821 } else {
822 decl
824 .replacen("type ", "export type ", 1)
825 .trim_end_matches(';')
826 .to_string()
827 };
828 parts.push(decl);
829 parts.push(String::new());
830 )+
831 };
832 }
833
834 collect_ts!(
835 SortOrder,
837 TimeRange,
838 LinkType,
839 AuthRegisterRequest,
841 LoginRequest,
842 AuthTokenResponse,
843 RefreshRequest,
844 LogoutRequest,
845 ChangePasswordRequest,
846 VerifyResponse,
847 UserSettingsResponse,
848 OkResponse,
849 IssueApiKeyResponse,
850 OAuthLinkResponse,
851 UploadResponse,
853 SessionSummary,
854 SessionListResponse,
855 SessionListQuery,
856 SessionDetail,
857 SessionLink,
858 ParseSource,
859 ParseCandidate,
860 ParsePreviewRequest,
861 ParsePreviewResponse,
862 ParsePreviewErrorResponse,
863 oauth::AuthProvidersResponse,
865 oauth::OAuthProviderInfo,
866 oauth::LinkedProvider,
867 HealthResponse,
869 CapabilitiesResponse,
870 ApiError,
871 );
872
873 let content = parts.join("\n");
874
875 if let Some(parent) = out_dir.parent() {
877 std::fs::create_dir_all(parent).ok();
878 }
879 let mut file = std::fs::File::create(&out_dir)
880 .unwrap_or_else(|e| panic!("Failed to create {}: {}", out_dir.display(), e));
881 file.write_all(content.as_bytes())
882 .unwrap_or_else(|e| panic!("Failed to write {}: {}", out_dir.display(), e));
883
884 println!("Generated TypeScript types at: {}", out_dir.display());
885 }
886}