Rust, Actix 및 MongoDB를 사용하여 GitHub 프로필 보기를 계산하는 API 구축

26842 단어 apigithubactixrust
이 게시물에서는 Rust, Actix 및 MongoDB를 사용하여 GitHub에서 프로필 보기를 계산하는 간단한 API를 빌드하는 방법을 배웁니다. 일반적인 HTTP API 프로젝트를 처음부터 시작하는 방법에 대한 자습서라고 생각할 수 있습니다.

조회수를 수집하는 아이디어는 간단합니다. 사용자가 수집된 통계를 보고 표시할 수 있는 github 페이지에 추적 이미지를 삽입해야 합니다. 수집된 통계를 표시하기 위해 shields 프로젝트를 사용합니다.
사용자 지정 데이터 공급자로 배지를 만들 수 있습니다.

전제 조건



먼저 개발을 시작하려면 적절한 도구를 설정해야 합니다. 최소한의 목록은 다음과 같습니다.

  • Rust Language - 응용 프로그램을 빌드하고 실행하기 위한 Rust SDK

  • VSCode - 응용 프로그램 소스 코드를 수정하는 편집기

  • Rust Analyzer - VSCode 확장
    Rust로 더 나은 개발 환경을 제공하는 것

  • Docker Desktop - Docker 이미지 빌드 또는 사용

  • Docker는 또한 운영 체제에 데이터베이스 인스턴스를 설치하지 않고 데이터베이스 인스턴스가 필요한 경우 애플리케이션을 로컬에서 개발하는 데 유용합니다.

    설치 프로젝트



    이 cargo 명령으로 Rust 프로젝트를 만들어 봅시다:

    cargo new counter
    


    이제 VSCode에서 프로젝트 폴더를 열고 HTTP API 개발에 필요한 종속성을 추가할 수 있습니다. 이 프로젝트에서 종속성은 다음과 같습니다.

  • Actix - 널리 사용되는 Rust 웹 프레임워크

  • MongoDB - MongoDB용 Rust 드라이버

  • Serde - JSON 직렬화/역직렬화를 위한 Rust 라이브러리
  • Cargo.toml 파일을 편집하거나 프로젝트 디렉토리에서 다음 명령을 사용하여 이를 수행할 수 있습니다.

    cargo add actix-web mongodb serde
    


    또한 보다 선언적인 직렬화를 위해 serde 매크로를 사용할 수 있도록 derive 기능 플래그를 수정해야 합니다.

    종속성 섹션은 다음과 같습니다.

    [dependencies]
    actix-web = "4.1.0"
    mongodb = "2.3.0"
    serde = { version = "1.0.140", features = ["derive"] }
    


    데이터 베이스



    코딩을 시작하기 전에 애플리케이션 데이터를 저장할 MongoDB 인스턴스가 필요합니다. DockerHub Registry의 공식 Docker 이미지를 사용하여 만들겠습니다.

    로컬 개발을 위한 Docker 컨테이너를 생성해 보겠습니다.

    docker run -d --name local-mongo \
        -p 27017:27017 \
        -e MONGO_INITDB_ROOT_USERNAME=admin \
        -e MONGO_INITDB_ROOT_PASSWORD=pass \
        mongo:latest
    


    완료되면 이 연결 문자열mongodb://admin:pass@localhost:27017을 사용하여 로컬 MongoDB에 연결할 수 있습니다.

    응용 프로그램이 데이터베이스에 액세스할 수 있도록 하려면 프로젝트의 config.toml 디렉터리에 있는 .cargo 파일에 연결 문자열을 추가해야 합니다.

    [env]
    DATABASE_URL = "mongodb://admin:pass@localhost:27017"
    


    MongoDB 컨테이너를 (재)시작하려면 이 명령이 필요할 수도 있습니다.

    docker restart local-mongo
    


    프로젝트 구조



    이 프로젝트에서는 코드를 구성하는 두 개의 레이어가 있습니다. 첫 번째 계층은 MongoDB 컬렉션을 사용하여 GitHub 프로필 보기를 추가하고 계산하는 데 사용할 데이터 서비스를 나타냅니다. 두 번째 레이어는 보기를 추적하고 수집된 통계를 검색하기 위한 API 끝점을 노출합니다.

    위에서 설명한 두 가지 기능을 사용하여 데이터 서비스를 만들어 봅시다.

    // service.rs
    
    // This structure represents a view event of a GitHub or Web page.
    #[derive(Serialize, Deserialize)]
    pub struct View {
        // The unique tracker name of this view event.
        name: String,
        // The date time of this view event.
        date: DateTime,
    }
    
    // The data service to add views and collect stats.
    #[derive(Clone)]
    pub struct ViewService {
        collection: Collection<View>,
    }
    
    impl ViewService {
        // Create an instance of the ViewService struct from a given db collection.
        pub fn new(collection: Collection<View>) -> Self {
            Self { collection }
        }
    
        // Register a view event in the storage.
        pub async fn add_view<T: AsRef<str>>(&self, name: T) -> Result<InsertOneResult> {
            let view = View {
                name: name.as_ref().to_owned(),
                date: DateTime::now(),
            };
            self.collection.insert_one(view, None).await
        }
    
        // Collect the view counts for a give unique tracker name.
        pub async fn get_view_count<T: AsRef<str>>(&self, name: T) -> Result<u64> {
            self.collection
                .count_documents(doc! {"name": name.as_ref()}, None)
                .await
        }
    }
    


    API용 엔드포인트 레이어:

    // endpoint.rs
    
    // The `shields.io` custom endpoint contract.
    #[derive(Serialize)]
    #[serde(rename_all = "camelCase")]
    pub struct Shield {
        schema_version: i32,
        label: String,
        message: String,
        cache_seconds: Option<u32>,
    }
    
    impl Shield {
        pub fn new(label: String, message: String) -> Self {
            Self {
                schema_version: 1,
                label,
                message,
                cache_seconds: Some(300),
            }
        }
    }
    
    #[derive(Deserialize)]
    pub struct ViewParams {
        name: String,
        label: Option<String>,
    }
    
    #[derive(Deserialize)]
    pub struct AddViewParams {
        name: String,
    }
    
    // Count the view events for a given unique tracker name.
    // The response is compatible with the `shields.io` project.
    // GET /views?name=NAME&label=LABEL
    #[get("/views")]
    pub async fn get_view_count(app_state: Data<AppState>, query: Query<ViewParams>) -> impl Responder {
        let ViewParams { name, label } = query.into_inner();
        if name.is_empty() {
            return HttpResponse::BadRequest().finish();
        }
    
        match app_state.view_service.get_view_count(name.as_str()).await {
            Ok(count) => {
                let label = label.unwrap_or_else(|| String::from("Views"));
                let message = count.to_string();
                let shield = Shield::new(label, message);
                HttpResponse::Ok().json(shield)
            }
            Err(_) => HttpResponse::InternalServerError().finish(),
        }
    }
    
    // Render a tracking pixel and register a view in the storage.
    // GET /tracker?name=NAME
    #[get("/tracker")]
    pub async fn add_view(app_state: Data<AppState>, query: Query<AddViewParams>) -> impl Responder {
        // Statically load the svg pixel image from the external file during compilation.
        // No need for escaping and other funny stuff.
        const PIXEL: &str = include_str!("pixel.svg");
        const SVG_MIME: &str = "image/svg+xml";
        // Disable caching to prevent GitHub or any other proxy to cache the rendered image.
        const CACHE_CONTROL: (&str, &str) = (
            "Cache-Control",
            "max-age=0, no-cache, no-store, must-revalidate",
        );
    
        let AddViewParams { name } = query.into_inner();
        if name.is_empty() {
            return HttpResponse::BadRequest().finish();
        }
    
        let _ = app_state.view_service.add_view(name).await;
        HttpResponse::Ok()
            .append_header(CACHE_CONTROL)
            .content_type(SVG_MIME)
            .body(PIXEL)
    }
    


    함께 정리해 보겠습니다.

    // main.rs
    
    // The settings to run an application instance.
    struct Settings {
        /// The web server port to run on.
        port: u16,
        /// The MongoDB database url.
        database_url: String,
    }
    
    impl Settings {
        // Create an instance of the Settings struct
        // from the environment variables.
        pub fn from_env() -> Self {
            let port: u16 = env::var("PORT")
                .expect("PORT expected")
                .parse()
                .expect("PORT must be a number");
            let database_url = env::var("DATABASE_URL").expect("DATABASE_URL expected");
            Self { port, database_url }
        }
    }
    
    // The shareable state for accessing
    // across different parts of the application.
    pub struct AppState {
        view_service: ViewService,
    }
    
    #[actix_web::main]
    async fn main() -> std::io::Result<()> {
        // Read the application settings from the env.
        let Settings { port, database_url } = Settings::from_env();
    
        // Create the database connection for the application.
        let options = ClientOptions::parse(database_url).await.unwrap();
        let client = Client::with_options(options).unwrap();
        let db = client.database("counter_db");
        let view_collection = db.collection::<View>("views");
    
        // Initialize and start the web server.
        HttpServer::new(move || {
            // Create the shareable state for the application.
            let view_service = ViewService::new(view_collection.clone());
            let app_state = AppState { view_service };
    
            // Create the application with the shareable state and
            // wired-up API endpoints.
            App::new()
                .app_data(Data::new(app_state))
                .service(endpoint::get_view_count)
                .service(endpoint::add_view)
        })
        .bind(("0.0.0.0", port))?
        .run()
        .await
    }
    


    웹 서버를 실행하려면 애플리케이션을 빌드하고 시작하십시오.

    cargo run
    


    결론



    따라서 이제 API 엔드포인트를 호출하고 자신만의 배지를 만들어 보기 통계를 표시할 수 있습니다.

    재료:
  • GitHub profile 의 보기를 수집하고 표시하는 방법의 예입니다.
  • 전체 소스 코드를 찾을 수 있습니다here.

  • 이 튜토리얼이 도움이 되었기를 바랍니다.

    좋은 웹페이지 즐겨찾기