Создание API для подсчета просмотров профиля на GitHub с помощью Rust, Actix и MongoDB

В этой статье мы узнаем, как построить простой API для подсчета просмотров профиля на GitHub с помощью Rust, Actix и MongoDB. Вы можете рассматривать его как учебник по созданию типичного проекта HTTP API с нуля.

Идея сбора количества просмотров проста. Нам нужно встроить изображение отслеживания в страницу github, которую пользователи смогут просматривать и отображать собранную статистику. Для отображения собранной статистики мы воспользуемся проектом shields
который позволяет создавать бейджи с пользовательскими поставщиками данных.

Предварительные условия

Прежде всего, вам необходимо установить соответствующие инструменты для начала разработки. Вот минимальный список:

  • Rust Language — Rust SDK для сборки и запуска приложений
  • VSCode — редактор для изменения исходного кода приложения
  • Rust Analyzer — расширение VSCode, которое обеспечивает лучший опыт разработки с Rust
  • Docker Desktop — для создания или использования образов Docker.

Docker также полезен для локальной разработки приложений в случае, если вам нужен экземпляр базы данных без установки ее в операционную систему.

Настройка проекта

Давайте создадим проект Rust с помощью этой команды cargo:

cargo new counter
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь вы можете открыть папку проекта в VSCode и добавить зависимости, необходимые для разработки HTTP API. В этом проекте зависимостями будут:

  • Actix — популярный веб-фреймворк на языке Rust
  • MongoDB — драйвер Rust для MongoDB
  • Serde — библиотека Rust для сериализации/десериализации JSON.

Вы можете сделать это либо редактируя файл 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 для хранения данных приложения. Мы создадим его, используя официальный образ Docker из реестра DockerHub.

Давайте создадим 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 с помощью строки подключения mongodb://admin:pass@localhost:27017.

Чтобы разрешить приложению доступ к базе данных, необходимо добавить строку подключения в файл 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.
  • Полный исходный код можно найти здесь.

Надеюсь, вы найдете этот урок полезным.

Оцените статью
devanswers.ru
Добавить комментарий