resemble_rust/
lib.rs

1const CONTENT_TYPE_APPLICATION_JSON: &'static str = "application/json";
2
3pub mod Resemble {
4    use lazy_static::lazy_static;
5    use std::sync::RwLock;
6
7    lazy_static! {
8        static ref API_TOKEN_RW: RwLock<String> = RwLock::new(String::new());
9    }
10
11    lazy_static! {
12        static ref BASE_URL_RW: RwLock<String> =
13            RwLock::new("https://app.resemble.ai/api/".to_string());
14    }
15
16    pub fn set_api_key(api_key: String) {
17        let mut api_token = API_TOKEN_RW.write().unwrap();
18        *api_token = api_key;
19    }
20
21    pub fn set_base_url(url: String) {
22        let mut base_url = BASE_URL_RW.write().unwrap();
23        *base_url = url;
24    }
25
26    fn token() -> String {
27        let api_token = API_TOKEN_RW.read();
28        let token = (*api_token.unwrap()).clone();
29
30        format!("Token token={}", token)
31    }
32
33    fn endpoint(version: &str, endpoint: &str) -> String {
34        let api_endpoint = if endpoint.starts_with('/') {
35            endpoint.to_string()
36        } else {
37            format!("/{}", endpoint)
38        };
39
40        let base_url = BASE_URL_RW.read();
41
42        format!("{}{}{}", *base_url.unwrap(), version, api_endpoint)
43    }
44
45    pub mod v2 {
46        use serde::{de, Deserialize};
47        type APIResponse<T> = Result<T, ErrorResponse>;
48
49        #[derive(Clone, Deserialize, Debug)]
50        pub struct ErrorResponse {
51            pub success: bool,
52            pub message: Option<String>,
53        }
54        
55        #[derive(Clone, Deserialize, Debug)]
56        pub struct MessageResponse {
57            pub success: bool,
58            pub message: Option<String>,
59        }
60
61        #[derive(Clone, Deserialize, Debug)]
62        pub struct PaginationResponse<T> {
63            pub success: bool,
64            pub message: Option<String>,
65            pub page: isize,
66            pub num_pages: isize,
67            pub page_size: isize,
68            pub items: Vec<T>,
69        }
70
71        #[derive(Clone, Deserialize, Debug)]
72        pub struct ReadResponse<T> {
73            pub success: bool,
74            pub message: Option<String>,
75            pub item: Option<T>,
76        }
77
78        #[derive(Clone, Deserialize, Debug)]
79        pub struct WriteResponse<T> {
80            pub success: bool,
81            pub message: Option<String>,
82            /* The item is returned when the write operation succeeds */
83            pub item: Option<T>,
84        }
85
86        #[derive(Clone, Deserialize, Debug)]
87        pub struct UpdateResponse<T> {
88            pub success: bool,
89            pub message: Option<String>,
90            /* The item is returned when the update operation succeeds */
91            pub item: Option<T>,
92        }
93
94        #[derive(Clone, Deserialize, Debug)]
95        pub struct DeleteResponse {
96            pub success: bool,
97            pub message: Option<String>,
98        }
99
100        fn return_json_error<T>(err: Option<reqwest::Error>) -> Result<T, ErrorResponse> {
101            Err(ErrorResponse {
102                message: Some(format!("Client error: {:#?}", err)),
103                success: false,
104            })
105        }
106
107        pub fn return_json_response<T: de::DeserializeOwned>(
108            response: Result<reqwest::blocking::Response, reqwest::Error>,
109        ) -> Result<T, ErrorResponse> {
110            if response.is_err() {
111                return return_json_error::<T>(response.err());
112            }
113
114            let response = response.unwrap();
115
116            if response.status().as_u16() == 401 {
117                let json = response.json::<ErrorResponse>();
118
119                if json.is_err() {
120                    return return_json_error::<T>(json.err());
121                }
122
123                Err(json.unwrap())
124            } else {
125                let json = response.json::<T>();
126
127                if json.is_err() {
128                    return return_json_error::<T>(json.err());
129                }
130
131                Ok(json.unwrap())
132            }
133        }
134
135        pub mod project {
136            use chrono::{DateTime, Utc};
137            use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
138            use serde::{Deserialize, Serialize};
139
140            use crate::Resemble::{
141                endpoint, token,
142                v2::{
143                    return_json_response, APIResponse, DeleteResponse, PaginationResponse,
144                    ReadResponse, UpdateResponse, WriteResponse,
145                },
146            };
147
148            #[derive(Clone, Deserialize, Debug)]
149            pub struct Project {
150                pub uuid: String,
151                pub name: String,
152                pub description: String,
153                pub is_public: bool,
154                pub is_collaborative: bool,
155                pub is_archived: bool,
156                pub created_at: DateTime<Utc>,
157                pub updated_at: DateTime<Utc>,
158            }
159
160            #[derive(Serialize, Debug)]
161            pub struct ProjectInput {
162                pub name: String,
163                pub description: String,
164                pub is_public: bool,
165                pub is_collaborative: bool,
166                pub is_archived: bool,
167            }
168
169            pub fn all(
170                page: usize,
171                page_size: Option<usize>,
172            ) -> APIResponse<PaginationResponse<Project>> {
173                let mut params = vec![("page", page)];
174                if page_size.is_some() {
175                    params.push(("page_size", page_size.unwrap()));
176                }
177
178                let response = reqwest::blocking::Client::new()
179                    .get(endpoint("v2", "projects"))
180                    .header(AUTHORIZATION, token())
181                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
182                    .query(&params)
183                    .send();
184
185                return_json_response(response)
186            }
187
188            pub fn create(project: ProjectInput) -> APIResponse<WriteResponse<Project>> {
189                let response = reqwest::blocking::Client::new()
190                    .post(endpoint("v2", "projects"))
191                    .header(AUTHORIZATION, token())
192                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
193                    .json(&project)
194                    .send();
195
196                return_json_response(response)
197            }
198
199            pub fn update(
200                uuid: String,
201                project: ProjectInput,
202            ) -> APIResponse<UpdateResponse<Project>> {
203                let response = reqwest::blocking::Client::new()
204                    .put(endpoint("v2", &format!("projects/{}", uuid)))
205                    .header(AUTHORIZATION, token())
206                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
207                    .json(&project)
208                    .send();
209
210                return_json_response(response)
211            }
212
213            pub fn get(uuid: String) -> APIResponse<ReadResponse<Project>> {
214                let response = reqwest::blocking::Client::new()
215                    .get(endpoint("v2", &format!("projects/{}", uuid)))
216                    .header(AUTHORIZATION, token())
217                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
218                    .send();
219
220                return_json_response(response)
221            }
222
223            pub fn delete(uuid: String) -> APIResponse<DeleteResponse> {
224                let response = reqwest::blocking::Client::new()
225                    .delete(endpoint("v2", &format!("projects/{}", uuid)))
226                    .header(AUTHORIZATION, token())
227                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
228                    .send();
229
230                return_json_response(response)
231            }
232        }
233
234        pub mod voice {
235            use chrono::{DateTime, Utc};
236            use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
237            use serde::{Deserialize, Serialize};
238
239            use crate::Resemble::{
240                endpoint, token,
241                v2::{
242                    return_json_response, APIResponse, DeleteResponse, PaginationResponse,
243                    ReadResponse, UpdateResponse, WriteResponse, MessageResponse
244                },
245            };
246
247            #[derive(Clone, Deserialize, Debug)]
248            pub struct Voice {
249                pub uuid: String,
250                pub name: String,
251                pub status: String,
252                pub dataset_url: Option<String>,
253                pub callback_uri: Option<String>,
254                pub created_at: DateTime<Utc>,
255                pub updated_at: DateTime<Utc>,
256            }
257
258            #[derive(Serialize, Debug)]
259            pub struct VoiceInput {
260                pub name: String,
261                pub dataset_url: Option<String>,
262                pub callback_uri: Option<String>
263            }
264
265            #[derive(Serialize, Debug)]
266            pub struct UpdateVoiceInput {
267                pub name: String,
268                pub callback_uri: Option<String>
269            }
270
271
272            pub fn all(
273                page: usize,
274                page_size: Option<usize>,
275            ) -> APIResponse<PaginationResponse<Voice>> {
276                let mut params = vec![("page", page)];
277                if page_size.is_some() {
278                    params.push(("page_size", page_size.unwrap()));
279                }
280
281                let response = reqwest::blocking::Client::new()
282                    .get(endpoint("v2", "voices"))
283                    .header(AUTHORIZATION, token())
284                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
285                    .query(&params)
286                    .send();
287
288                return_json_response(response)
289            }
290
291            pub fn create(voice: VoiceInput) -> APIResponse<WriteResponse<Voice>> {
292                let response = reqwest::blocking::Client::new()
293                    .post(endpoint("v2", "voices"))
294                    .header(AUTHORIZATION, token())
295                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
296                    .json(&voice)
297                    .send();
298
299                return_json_response(response)
300            }
301
302            pub fn update(uuid: String, voice: UpdateVoiceInput) -> APIResponse<UpdateResponse<Voice>> {
303                let response = reqwest::blocking::Client::new()
304                    .put(endpoint("v2", &format!("voices/{}", uuid)))
305                    .header(AUTHORIZATION, token())
306                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
307                    .json(&voice)
308                    .send();
309
310                return_json_response(response)
311            }
312            
313            pub fn build(uuid: String) -> APIResponse<MessageResponse> {
314                let response = reqwest::blocking::Client::new()
315                    .post(endpoint("v2", &format!("voices/{}/build", uuid)))
316                    .header(AUTHORIZATION, token())
317                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
318                    .send();
319                    
320                return_json_response(response)
321            }
322
323            pub fn get(uuid: String) -> APIResponse<ReadResponse<Voice>> {
324                let response = reqwest::blocking::Client::new()
325                    .get(endpoint("v2", &format!("voices/{}", uuid)))
326                    .header(AUTHORIZATION, token())
327                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
328                    .send();
329
330                return_json_response(response)
331            }
332
333            pub fn delete(uuid: String) -> APIResponse<DeleteResponse> {
334                let response = reqwest::blocking::Client::new()
335                    .delete(endpoint("v2", &format!("voices/{}", uuid)))
336                    .header(AUTHORIZATION, token())
337                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
338                    .send();
339
340                return_json_response(response)
341            }
342        }
343
344        pub mod recording {
345            use chrono::{DateTime, Utc};
346            use reqwest::blocking::multipart::Form;
347            use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
348            use serde::{Deserialize, Serialize};
349
350            use crate::Resemble::{
351                endpoint, token,
352                v2::{
353                    return_json_error, return_json_response, APIResponse, DeleteResponse,
354                    ErrorResponse, PaginationResponse, ReadResponse, UpdateResponse, WriteResponse,
355                },
356            };
357
358            #[derive(Clone, Deserialize, Debug)]
359            pub struct Recording {
360                pub uuid: String,
361                pub name: String,
362                pub text: String,
363                pub emotion: String,
364                pub is_active: bool,
365                pub audio_src: String,
366                pub created_at: DateTime<Utc>,
367                pub updated_at: DateTime<Utc>,
368            }
369
370            #[derive(Serialize, Debug)]
371            pub struct RecordingInput {
372                pub name: String,
373                pub text: String,
374                pub emotion: String,
375                pub is_active: bool,
376            }
377
378            pub fn all(
379                voice_uuid: String,
380                page: usize,
381                page_size: Option<usize>,
382            ) -> APIResponse<PaginationResponse<Recording>> {
383                let mut params = vec![("page", page)];
384                if page_size.is_some() {
385                    params.push(("page_size", page_size.unwrap()));
386                }
387
388                let response = reqwest::blocking::Client::new()
389                    .get(endpoint("v2", &format!("voices/{}/recordings", voice_uuid)))
390                    .header(AUTHORIZATION, token())
391                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
392                    .query(&params)
393                    .send();
394
395                return_json_response(response)
396            }
397
398            pub fn create(
399                voice_uuid: String,
400                recording: RecordingInput,
401                audio_file: std::path::PathBuf,
402            ) -> APIResponse<WriteResponse<Recording>> {
403                let form = Form::new()
404                    .text("name", recording.name)
405                    .text("text", recording.text)
406                    .text("emotion", recording.emotion)
407                    .text(
408                        "is_active",
409                        if recording.is_active { "true" } else { "false" },
410                    )
411                    .file("file", audio_file);
412
413                if form.is_err() {
414                    return Err(ErrorResponse {
415                        message: Some(format!("Client error: {:#?}", form.err())),
416                        success: false,
417                    });
418                }
419
420                let response = reqwest::blocking::Client::new()
421                    .post(endpoint("v2", &format!("voices/{}/recordings", voice_uuid)))
422                    .multipart(form.unwrap())
423                    .header(AUTHORIZATION, token())
424                    .send();
425
426                return_json_response(response)
427            }
428
429            pub fn update(
430                voice_uuid: String,
431                uuid: String,
432                recording: RecordingInput,
433            ) -> APIResponse<UpdateResponse<Recording>> {
434                let response = reqwest::blocking::Client::new()
435                    .put(endpoint(
436                        "v2",
437                        &format!("voices/{}/recordings/{}", voice_uuid, uuid),
438                    ))
439                    .header(AUTHORIZATION, token())
440                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
441                    .json(&recording)
442                    .send();
443
444                return_json_response(response)
445            }
446
447            pub fn get(voice_uuid: String, uuid: String) -> APIResponse<ReadResponse<Recording>> {
448                let response = reqwest::blocking::Client::new()
449                    .get(endpoint(
450                        "v2",
451                        &format!("voices/{}/recordings/{}", voice_uuid, uuid),
452                    ))
453                    .header(AUTHORIZATION, token())
454                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
455                    .send();
456
457                return_json_response(response)
458            }
459
460            pub fn delete(voice_uuid: String, uuid: String) -> APIResponse<DeleteResponse> {
461                let response = reqwest::blocking::Client::new()
462                    .delete(endpoint(
463                        "v2",
464                        &format!("voices/{}/recordings/{}", voice_uuid, uuid),
465                    ))
466                    .header(AUTHORIZATION, token())
467                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
468                    .send();
469
470                return_json_response(response)
471            }
472        }
473
474        pub mod clip {
475            use chrono::{DateTime, Utc};
476            use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
477            use serde::{Deserialize, Serialize};
478
479            use crate::Resemble::{
480                endpoint, token,
481                v2::{
482                    return_json_error, return_json_response, APIResponse, DeleteResponse,
483                    ErrorResponse, PaginationResponse, ReadResponse, UpdateResponse, WriteResponse,
484                },
485            };
486
487            #[derive(Clone, Deserialize, Debug)]
488            pub struct Timestamps {
489                pub phonemes: String,
490                pub end_times: Vec<f64>,
491                pub phoneme_chars: Vec<String>,
492            }
493
494            #[derive(Clone, Deserialize, Debug)]
495            pub struct Clip {
496                pub uuid: String,
497                pub title: Option<String>,
498                pub body: String,
499                pub voice_uuid: String,
500                pub is_public: bool,
501                pub is_archived: bool,
502                pub timestamps: Option<Timestamps>,
503                pub audio_src: Option<String>,
504                pub raw_audio: Option<String>,
505                pub created_at: DateTime<Utc>,
506                pub updated_at: DateTime<Utc>,
507            }
508
509            #[derive(Serialize, Debug)]
510            pub struct SyncClipInput {
511                pub title: Option<String>,
512                pub body: String,
513                pub voice_uuid: String,
514                pub is_public: bool,
515                pub is_archived: bool,
516                pub sample_rate: Option<usize>, // 16000 | 22050 | 44100
517                pub output_format: Option<String>, // 'wav' | 'mp3'
518                pub precision: Option<String>,  // 'PCM_16' | 'PCM_32'
519                pub include_timestamps: Option<bool>,
520
521                pub raw: Option<bool>,
522            }
523
524            #[derive(Serialize, Debug)]
525            pub struct AsyncClipInput {
526                pub title: Option<String>,
527                pub body: String,
528                pub voice_uuid: String,
529                pub is_public: bool,
530                pub is_archived: bool,
531                pub sample_rate: Option<usize>, // 16000 | 22050 | 44100
532                pub output_format: Option<String>, // 'wav' | 'mp3'
533                pub precision: Option<String>,  // 'PCM_16' | 'PCM_32'
534                pub include_timestamps: Option<bool>,
535
536                pub callback_uri: String,
537            }
538
539            pub fn all(
540                project_uuid: String,
541                page: usize,
542                page_size: Option<usize>,
543            ) -> APIResponse<PaginationResponse<Clip>> {
544                let mut params = vec![("page", page)];
545                if page_size.is_some() {
546                    params.push(("page_size", page_size.unwrap()));
547                }
548
549                let response = reqwest::blocking::Client::new()
550                    .get(endpoint("v2", &format!("projects/{}/clips", project_uuid)))
551                    .header(AUTHORIZATION, token())
552                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
553                    .query(&params)
554                    .send();
555
556                return_json_response(response)
557            }
558
559            pub fn create_sync(
560                project_uuid: String,
561                clip: SyncClipInput,
562            ) -> APIResponse<WriteResponse<Clip>> {
563                let response = reqwest::blocking::Client::new()
564                    .post(endpoint("v2", &format!("projects/{}/clips", project_uuid)))
565                    .header(AUTHORIZATION, token())
566                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
567                    .json(&clip)
568                    .send();
569
570                return_json_response(response)
571            }
572
573            pub fn create_async(
574                project_uuid: String,
575                clip: AsyncClipInput,
576            ) -> APIResponse<WriteResponse<Clip>> {
577                let response = reqwest::blocking::Client::new()
578                    .post(endpoint("v2", &format!("projects/{}/clips", project_uuid)))
579                    .header(AUTHORIZATION, token())
580                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
581                    .json(&clip)
582                    .send();
583
584                return_json_response(response)
585            }
586
587            pub fn update_async(
588                project_uuid: String,
589                uuid: String,
590                clip: AsyncClipInput,
591            ) -> APIResponse<UpdateResponse<Clip>> {
592                let response = reqwest::blocking::Client::new()
593                    .put(endpoint(
594                        "v2",
595                        &format!("projects/{}/clips/{}", project_uuid, uuid),
596                    ))
597                    .header(AUTHORIZATION, token())
598                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
599                    .json(&clip)
600                    .send();
601
602                return_json_response(response)
603            }
604
605            pub fn get(project_uuid: String, uuid: String) -> APIResponse<ReadResponse<Clip>> {
606                let response = reqwest::blocking::Client::new()
607                    .get(endpoint(
608                        "v2",
609                        &format!("projects/{}/clips/{}", project_uuid, uuid),
610                    ))
611                    .header(AUTHORIZATION, token())
612                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
613                    .send();
614
615                return_json_response(response)
616            }
617
618            pub fn delete(project_uuid: String, uuid: String) -> APIResponse<DeleteResponse> {
619                let response = reqwest::blocking::Client::new()
620                    .delete(endpoint(
621                        "v2",
622                        &format!("projects/{}/clips/{}", project_uuid, uuid),
623                    ))
624                    .header(AUTHORIZATION, token())
625                    .header(CONTENT_TYPE, crate::CONTENT_TYPE_APPLICATION_JSON)
626                    .send();
627
628                return_json_response::<DeleteResponse>(response)
629            }
630        }
631    }
632}
633
634#[cfg(test)]
635mod tests {
636    use crate::Resemble::{
637        self,
638        v2::clip::{AsyncClipInput, SyncClipInput},
639        v2::project::ProjectInput,
640        v2::recording::RecordingInput,
641        v2::voice::VoiceInput,
642    };
643    use std::{env::var, panic};
644
645    fn get_test_voice_uuid() -> String {
646        std::env::var("TEST_VOICE_UUID")
647            .expect("Invalid voice_uuid; please set the TEST_VOICE_UUID environment variable")
648    }
649
650    fn get_test_callback_uri() -> String {
651        std::env::var("TEST_CALLBACK_URI")
652            .expect("Invalid callback_uri; please set the TEST_CALLBACK_URI environment variable")
653    }
654
655    #[test]
656    fn test_v2_projects() {
657        run_test(|| {
658            // all()
659            let projects = Resemble::v2::project::all(1, None).unwrap();
660            assert_eq!(projects.success, true);
661
662            // create()
663            let project = Resemble::v2::project::create(ProjectInput {
664                name: "Test Project".to_string(),
665                description: "Test Description".to_string(),
666                is_archived: false,
667                is_collaborative: false,
668                is_public: false,
669            })
670            .unwrap();
671            assert_eq!(project.success, true);
672
673            // update()
674            let updated_project = Resemble::v2::project::update(
675                project.item.unwrap().uuid,
676                ProjectInput {
677                    name: "Updated Test Project".to_string(),
678                    description: "Updated Test Description".to_string(),
679                    is_archived: false,
680                    is_collaborative: false,
681                    is_public: false,
682                },
683            )
684            .unwrap();
685            assert_eq!(updated_project.success, true);
686
687            // get()
688            let fetched_project =
689                Resemble::v2::project::get(updated_project.item.unwrap().uuid).unwrap();
690            assert_eq!(fetched_project.success, true);
691
692            // delete()
693            let delete_op =
694                Resemble::v2::project::delete(fetched_project.item.unwrap().uuid).unwrap();
695            assert_eq!(delete_op.success, true);
696        })
697    }
698
699    #[test]
700    fn test_v2_voices() {
701        run_test(|| {
702            // all()
703            let voices = Resemble::v2::voice::all(1, None).unwrap();
704            assert_eq!(voices.success, true);
705
706            // create()
707            let voice = Resemble::v2::voice::create(VoiceInput {
708                name: "Test voice".to_string(),
709            })
710            .unwrap();
711            assert_eq!(voice.success, true);
712
713            // update()
714            let updated_voice = Resemble::v2::voice::update(
715                voice.item.unwrap().uuid,
716                VoiceInput {
717                    name: "Updated Test voice".to_string(),
718                },
719            )
720            .unwrap();
721            assert_eq!(updated_voice.success, true);
722
723            // get()
724            let fetched_voice = Resemble::v2::voice::get(updated_voice.item.unwrap().uuid).unwrap();
725            assert_eq!(fetched_voice.success, true);
726
727            // delete()
728            let delete_op = Resemble::v2::voice::delete(fetched_voice.item.unwrap().uuid).unwrap();
729            assert_eq!(delete_op.success, true);
730        })
731    }
732
733    #[test]
734    fn test_v2_clips() {
735        run_test(|| {
736            let project = Resemble::v2::project::create(ProjectInput {
737                description: "Test Description".to_string(),
738                name: "Test Project".to_string(),
739                is_archived: false,
740                is_collaborative: false,
741                is_public: false,
742            })
743            .unwrap();
744            let project_uuid = project.item.unwrap().uuid;
745
746            // all()
747            let clips = Resemble::v2::clip::all(project_uuid.clone(), 1, None).unwrap();
748            assert_eq!(clips.success, true);
749
750            // create_sync()
751            let clip_create_sync = Resemble::v2::clip::create_sync(
752                project_uuid.clone(),
753                SyncClipInput {
754                    body: "this is a test".to_string(),
755                    include_timestamps: Some(true),
756                    is_archived: false,
757                    is_public: false,
758                    output_format: None,
759                    precision: None,
760                    raw: None,
761                    sample_rate: None,
762                    title: None,
763                    voice_uuid: get_test_voice_uuid(),
764                },
765            )
766            .unwrap();
767            assert_eq!(clip_create_sync.success, true);
768
769            let clip_uuid = clip_create_sync.clone().item.unwrap().uuid;
770
771            // create_async()
772            let clip_create_async = Resemble::v2::clip::create_async(
773                project_uuid.clone(),
774                AsyncClipInput {
775                    body: "this is a test".to_string(),
776                    include_timestamps: Some(true),
777                    is_archived: false,
778                    is_public: false,
779                    output_format: None,
780                    precision: None,
781                    sample_rate: None,
782                    title: None,
783                    voice_uuid: get_test_voice_uuid(),
784                    callback_uri: get_test_callback_uri(),
785                },
786            )
787            .unwrap();
788            assert_eq!(clip_create_async.success, true);
789
790            // update_async()
791            let clip_update_async = Resemble::v2::clip::update_async(
792                project_uuid.clone(),
793                clip_uuid.clone(),
794                AsyncClipInput {
795                    body: "this is another test".to_string(),
796                    include_timestamps: Some(true),
797                    is_archived: false,
798                    is_public: false,
799                    output_format: None,
800                    precision: None,
801                    sample_rate: None,
802                    title: None,
803                    voice_uuid: get_test_voice_uuid(),
804                    callback_uri: get_test_callback_uri(),
805                },
806            )
807            .unwrap();
808            assert_eq!(clip_update_async.success, true);
809
810            // get()
811            let fetched_clip =
812                Resemble::v2::clip::get(project_uuid.clone(), clip_uuid.clone()).unwrap();
813            assert_eq!(fetched_clip.success, true);
814
815            // delete()
816            let delete_op =
817                Resemble::v2::clip::delete(project_uuid.clone(), clip_uuid.clone()).unwrap();
818            assert_eq!(delete_op.success, true);
819        })
820    }
821
822    #[test]
823    fn test_v2_recordings() {
824        run_test(|| {
825            let voice = Resemble::v2::voice::create(VoiceInput {
826                name: "Test Voice".to_string(),
827            })
828            .unwrap();
829            let voice_uuid = voice.item.unwrap().uuid;
830
831            // all()
832            let recordings = Resemble::v2::recording::all(voice_uuid.clone(), 1, None).unwrap();
833            assert_eq!(recordings.success, true);
834
835            let path = std::path::PathBuf::from("spec_sample_audio.wav");
836            assert!(path.exists());
837
838            // create()
839            let created_recording = Resemble::v2::recording::create(
840                voice_uuid.clone(),
841                RecordingInput {
842                    emotion: "neutral".to_string(),
843                    is_active: true,
844                    name: "recording".to_string(),
845                    text: "this is a test".to_string(),
846                },
847                path,
848            )
849            .unwrap();
850            assert_eq!(created_recording.success, true);
851
852            let recording_uuid = created_recording.clone().item.unwrap().uuid;
853
854            // update()
855            let updated_recording = Resemble::v2::recording::update(
856                voice_uuid.clone(),
857                recording_uuid.clone(),
858                RecordingInput {
859                    emotion: "neutral".to_string(),
860                    is_active: false,
861                    name: "updated recording".to_string(),
862                    text: "this is an updated test".to_string(),
863                },
864            )
865            .unwrap();
866            assert_eq!(updated_recording.success, true);
867
868            // get()
869            let fetched_recording =
870                Resemble::v2::recording::get(voice_uuid.clone(), recording_uuid.clone()).unwrap();
871            assert_eq!(fetched_recording.success, true);
872
873            // delete()
874            let delete_op =
875                Resemble::v2::recording::delete(voice_uuid.clone(), recording_uuid.clone())
876                    .unwrap();
877            assert_eq!(delete_op.success, true);
878        })
879    }
880
881    fn run_test<T>(test: T) -> ()
882    where
883        T: FnOnce() -> () + panic::UnwindSafe,
884    {
885        let api_key = var("TEST_API_KEY").unwrap();
886        let base_url = var("TEST_BASE_URL").unwrap_or_default();
887        Resemble::set_api_key(api_key);
888
889        if !base_url.is_empty() {
890            Resemble::set_base_url(base_url);
891        }
892
893        let result = panic::catch_unwind(|| test());
894
895        assert!(result.is_ok())
896    }
897}