@@ -10,6 +10,9 @@ use std::time::Duration;
1010use tracing:: { Level , error, info} ;
1111use tracing_subscriber:: FmtSubscriber ;
1212
13+ const GITHUB_BASE_URL : & str = "https://github.com/" ;
14+ const GITHUB_URL_FORMAT : & str = "https://github.com/{owner}/{repo_name}" ;
15+
1316async fn spawn_repository_checker (
1417 repo_url : & str ,
1518 branch : Option < String > ,
@@ -33,20 +36,20 @@ async fn health_check() -> &'static str {
3336
3437#[ derive( Deserialize ) ]
3538struct CheckRequest {
36- repo_url : String ,
39+ repo : RepositoryURL ,
3740 branch : Option < String > ,
3841 interval_secs : Option < u64 > ,
3942}
4043
4144async fn check_handler ( Json ( payload) : Json < CheckRequest > ) -> Result < & ' static str , StatusCode > {
4245 info ! (
4346 "Received check request for repository: {}, branch: {:?}" ,
44- payload. repo_url , payload. branch
47+ payload. repo . url , payload. branch
4548 ) ;
4649 // FIXME 일단 interval_secs 는 유저가 수정할 수 없게 할 거긴 한데, 일단 테스트할 때 편하게 요청을 받아보자.
4750 let interval = payload. interval_secs . unwrap_or ( 120 ) ;
4851 let interval = Duration :: from_secs ( interval) ;
49- if let Err ( e) = spawn_repository_checker ( & payload. repo_url , payload. branch , interval) . await {
52+ if let Err ( e) = spawn_repository_checker ( & payload. repo . url , payload. branch , interval) . await {
5053 error ! ( "Failed to spawn repository checker: {}" , e) ;
5154 return Err ( StatusCode :: BAD_REQUEST ) ;
5255 }
@@ -55,12 +58,56 @@ async fn check_handler(Json(payload): Json<CheckRequest>) -> Result<&'static str
5558
5659#[ derive( Deserialize ) ]
5760struct CancelRequest {
58- repo_url : String ,
61+ repo : RepositoryURL ,
5962 branch : Option < String > ,
6063}
6164
65+ /// Represents a GitHub repository URL.
66+ ///
67+ /// This struct ensures that the URL is valid and follows the format
68+ /// `https://github.com/{owner}/{repo_name}`. It includes validation logic
69+ /// to enforce this format.
70+ #[ derive( Debug , Clone ) ]
71+ struct RepositoryURL {
72+ /// The URL of the repository.
73+ url : String ,
74+ }
75+
76+ impl < ' de > Deserialize < ' de > for RepositoryURL {
77+ /// Custom deserialization logic for `RepositoryURL`.
78+ ///
79+ /// This implementation ensures that the URL is validated during
80+ /// deserialization. If the URL is invalid, an error is returned.
81+ fn deserialize < D > ( deserializer : D ) -> Result < Self , D :: Error >
82+ where
83+ D : serde:: Deserializer < ' de > ,
84+ {
85+ let url = String :: deserialize ( deserializer) ?;
86+ let repo = RepositoryURL { url } ;
87+ repo. validate ( ) . map_err ( serde:: de:: Error :: custom) ?;
88+ Ok ( repo)
89+ }
90+ }
91+
92+ impl RepositoryURL {
93+ fn validate ( & self ) -> Result < ( ) , String > {
94+ if !self . url . starts_with ( GITHUB_BASE_URL ) {
95+ return Err ( format ! ( "URL must start with {}" , GITHUB_BASE_URL ) ) ;
96+ }
97+ let parts: Vec < & str > = self
98+ . url
99+ . trim_start_matches ( GITHUB_BASE_URL )
100+ . split ( '/' )
101+ . collect ( ) ;
102+ if parts. len ( ) != 2 || parts[ 0 ] . is_empty ( ) || parts[ 1 ] . is_empty ( ) {
103+ return Err ( format ! ( "URL must be in format {}" , GITHUB_URL_FORMAT ) ) ;
104+ }
105+ Ok ( ( ) )
106+ }
107+ }
108+
62109async fn cancel_handler ( Json ( payload) : Json < CancelRequest > ) -> Result < & ' static str , StatusCode > {
63- if let Err ( e) = cancel_repository_checker ( & payload. repo_url , payload. branch ) . await {
110+ if let Err ( e) = cancel_repository_checker ( & payload. repo . url , payload. branch ) . await {
64111 error ! ( "Repository checker failed: {}" , e) ;
65112 return Err ( StatusCode :: BAD_REQUEST ) ;
66113 }
0 commit comments