العودة إلى المدونة

واجهة برمجة تطبيقات أسعار صرف العملات في Rust — دليل التكامل الكامل

V
Vlado Grigirov
August 04, 2026
Rust Currency API Exchange Rates Tutorial reqwest Tokio Fintech

إذا كنت تبني بنية تحتية للتقنية المالية، أو محرك تسعير، أو أداة سطر أوامر، أو خلفية عالية الإنتاجية، فعاجلاً أم آجلاً ستحتاج إلى بيانات صرف عملات أجنبية حية. إن ربط واجهة برمجة تطبيقات أسعار صرف العملات في Rust يُعد من أنظف مهام الشبكات التي تقدمها اللغة: فمع reqwest وtokio وserde تحصل على عميل HTTP غير متزامن، وإلغاء تسلسل JSON تلقائي، وضمانات في وقت الترجمة بأن شيفرة معالجة الأسعار لديك صحيحة قبل أن تعمل أصلاً. يمر هذا الدليل عبر المسار بأكمله — من أول طلب GET إلى محوّل عملات بشكل إنتاجي مع عميل قابل لإعادة الاستخدام، ونماذج مُصنّفة بأنواع، ومعالجة أخطاء مخصصة، وحسابات نقدية آمنة بالأرقام العشرية، وذاكرة تخزين مؤقت داخلية تُبقيك ضمن الباقة المجانية.

في النهاية سيكون لديك وحدة صغيرة قابلة لإعادة الاستخدام في Rust مبنية على توثيق Finexly API تُحوّل بين أيٍّ من أكثر من 170 عملة بأسعار فورية. إذا كنت قد قرأت بالفعل دليل واجهة برمجة تطبيقات العملات في Go، فهذا هو نظيره في Rust بالبنية نفسها — عميل مُصنّف بأنواع، وذاكرة تخزين مؤقت، وتمرير أخطاء نظيف.

لماذا تُعد Rust ملائمة تمامًا لبيانات العملات

بيانات العملات مرتبطة بالإدخال والإخراج، وحساسة للكمون، و — في أي نظام يتعامل مع المال — حرجة من حيث الصحة. Rust ملائمة بشكل غير عادي لهذه الجوانب الثلاثة جميعها:

  • غير متزامنة افتراضيًا. reqwest مبني على hyper وtokio، لذا فإن جلب الأسعار المتزامن رخيص وغير حاجب. يمكنك طلب عشرات أزواج العملات على التوازي دون إنشاء خيوط نظام تشغيل.
  • نظام الأنواع يلتقط الأخطاء مبكرًا. إن بنية مُشتقة عبر serde تعني أن استجابة مشوّهة أو حقلًا مفقودًا يصبح خطأ في وقت الترجمة أو وقت إلغاء التسلسل، وليس قيمة null غامضة تحت ثلاث طبقات في الإنتاج.
  • لا يوجد جامع نفايات، وكمون متوقع. بالنسبة لخدمة تقدّم أسعار صرف أجنبي تحت الحمل، فإن غياب توقفات جامع النفايات أمر مهم.
  • أرقام عشرية آمنة للمال. مع صندوق rust_decimal تحصل على حسابات ذات فاصلة ثابتة، فلا تُطلق أبدًا خطأ تقريب يحوّل €100.00 إلى €99.999999.

المقايضة هي إعداد أشدّ حدة قليلاً من لغة برمجة نصية، لكن النتيجة عميل عملات يمكنك إدراجه في خدمة والوثوق به.

ما الذي ستبنيه

عميل FinexlyClient قابل لإعادة الاستخدام يقوم بما يلي:

  1. يجلب أحدث الأسعار لعملة أساس من واجهة برمجة تطبيقات أسعار صرف العملات.
  2. يُلغي تسلسل JSON إلى بنى Rust مُصنّفة بأنواع باستخدام serde.
  3. يُحوّل بين أي عملتين، حتى عندما لا تكون أيٌّ منهما هي الأساس (الأسعار المتقاطعة).
  4. يعالج الأخطاء — أعطال الشبكة، والاستجابات غير 200، وحدود المعدل — بنوع خطأ مخصص.
  5. يُخزّن الاستجابات مؤقتًا في الذاكرة بمدة صلاحية (TTL) لتبقى مرتاحًا ضمن الباقة المجانية.

المتطلبات المسبقة وإعداد المشروع

تحتاج إلى سلسلة أدوات Rust مستقرة وحديثة (ثبّتها عبر rustup) ومفتاح Finexly API مجاني. يمكنك التسجيل مجانًا في أقل من دقيقة — دون بطاقة ائتمان، و1000 طلب شهريًا في الباقة المجانية.

أنشئ مشروعًا جديدًا وأضف التبعيات:

cargo new finexly-rates
cd finexly-rates
cargo add tokio --features full
cargo add reqwest --features json
cargo add serde --features derive
cargo add thiserror
cargo add rust_decimal rust_decimal_macros

يجب أن تبدو تبعيات Cargo.toml لديك الآن هكذا:

[dependencies]
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
thiserror = "2"
rust_decimal = "1"
rust_decimal_macros = "1"

ملاحظة سريعة حول الصناديق: reqwest هو عميل HTTP غير المتزامن الفعلي في Rust؛ وميزة json تجلب serde_json وتُمكّن المساعد .json(). أما serde مع derive فيمنحك #[derive(Deserialize)]. وthiserror يجعل تعدادات الأخطاء المخصصة مريحة. وrust_decimal يوفّر أرقامًا عشرية ذات فاصلة ثابتة للمال.

خزّن مفتاحك في متغير بيئة كي لا يصل أبدًا إلى نظام التحكم بالمصادر:

export FINEXLY_API_KEY="your_api_key_here"

شكل استجابة Finexly API

قبل كتابة أي شيفرة، انظر إلى ما تُرجعه نقطة النهاية. طلب إلى نقطة نهاية أحدث الأسعار:

curl "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey=YOUR_KEY"

يُرجع كائن JSON صغيرًا ومتوقعًا:

{
  "success": true,
  "base": "USD",
  "timestamp": 1753660800,
  "rates": {
    "EUR": 0.9213,
    "GBP": 0.7847,
    "JPY": 161.42
  }
}

يُشكّل أمران الشيفرة أدناه. أولاً، rates هو خريطة مسطّحة من رمز عملة ISO 4217 إلى رقم بفاصلة عائمة — وهذا يتوافق بسلاسة مع نوع Rust HashMap<String, f64>. ثانيًا، base يخبرك بما تنسب إليه هذه الأسعار. لتحويل USD → EUR تضرب في rates["EUR"]. ولتحويل بين عملتين غير أساسيتين، تمر عبر الأساس: تقسم على واحدة، وتضرب في الأخرى.

إجراء أول طلب لك باستخدام reqwest

ابدأ بأبسط طلب غير متزامن ممكن. استبدل src/main.rs بما يلي:

#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
    let api_key = std::env::var("FINEXLY_API_KEY")
        .expect("Set FINEXLY_API_KEY in your environment");

    let url = format!(
        "https://api.finexly.com/v1/latest?base=USD&symbols=EUR,GBP,JPY&apikey={api_key}"
    );

    let body = reqwest::get(&url).await?.text().await?;
    println!("{body}");

    Ok(())
}

شغّله بالأمر cargo run وسترى JSON الخام مطبوعًا. ماكرو #[tokio::main] يُعِدّ بيئة التشغيل غير المتزامنة، وreqwest::get ينفّذ الطلب، وكل .await? يُعلّق المهمة حتى تستجيب الشبكة مع تمرير أي خطأ. هذا يعمل، لكن reqwest::get يُنشئ عميلاً جديدًا تمامًا في كل استدعاء — وهذا مقبول للاستخدام لمرة واحدة، لكنه خطأ في تطبيق حقيقي. سنُصلح ذلك قريبًا.

نمذجة JSON باستخدام Serde

طباعة سلسلة نصية ليست مفيدة؛ أنت تريد بيانات مُصنّفة بأنواع. عرّف بنى تعكس الاستجابة ودع serde يقوم بالتحليل:

use serde::Deserialize;
use std::collections::HashMap;

#[derive(Debug, Deserialize)]
pub struct RatesResponse {
    pub success: bool,
    pub base: String,
    pub timestamp: i64,
    pub rates: HashMap<String, f64>,
}

الآن بدّل .text() بـ .json()، الذي يُلغي تسلسل الجسم مباشرة إلى بنيتك:

let data: RatesResponse = reqwest::get(&url).await?.json().await?;
println!("1 USD = {} EUR", data.rates["EUR"]);

يقرأ الأسلوب json() جسم الاستجابة ويُشغّل serde_json تحت الغطاء. إذا كان حقل مفقودًا أو من نوع خاطئ، فستحصل على خطأ إلغاء تسلسل واضح بدلاً من قيمة null صامتة. ولأن rates هو HashMap، يمكنك البحث عن أي عملة مُرجَعة برمز ISO 4217 الخاص بها.

بناء عميل قابل لإعادة الاستخدام

أكثر أخطاء reqwest شيوعًا هو إنشاء Client جديد لكل طلب. كل عميل يملك مجمّع اتصالاته الخاص، لذا فإن إعادة إنشائه تُهدر اتصالات keep-alive ومصافحات TLS. أنشئ Client واحدًا، واستنسخه بتكلفة زهيدة (فهو Arc داخليًا)، وأعِد استخدامه في كل مكان. غلّفه في بنية صغيرة تُخفي عنوان URL والمفتاح:

use reqwest::Client;

#[derive(Clone)]
pub struct FinexlyClient {
    http: Client,
    api_key: String,
    base_url: String,
}

impl FinexlyClient {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            http: Client::builder()
                .timeout(std::time::Duration::from_secs(10))
                .build()
                .expect("failed to build HTTP client"),
            api_key: api_key.into(),
            base_url: "https://api.finexly.com/v1".to_string(),
        }
    }

    pub async fn latest(
        &self,
        base: &str,
        symbols: &[&str],
    ) -> Result<RatesResponse, FinexlyError> {
        let url = format!("{}/latest", self.base_url);
        let symbols_csv = symbols.join(",");

        let resp = self
            .http
            .get(&url)
            .query(&[
                ("base", base),
                ("symbols", symbols_csv.as_str()),
                ("apikey", self.api_key.as_str()),
            ])
            .send()
            .await?;

        let resp = resp.error_for_status()?;
        let data = resp.json::<RatesResponse>().await?;
        Ok(data)
    }
}

بضعة أمور تستحق الإشارة. يتولى البنّاء .query(&[...]) ترميز عنوان URL نيابة عنك، فلا تحتاج أبدًا إلى دمج سلاسل الاستعلام يدويًا. و.timeout(...) في البنّاء يحمي من أن يُوقف اتصال معلّق خدمتك. وerror_for_status() يُحوّل أي استجابة HTTP غير 2xx إلى Err — وهو ما يقودنا إلى معالجة الأخطاء.

معالجة أخطاء متينة

يجب أن تُجيب الشيفرة الإنتاجية عن السؤال: ماذا يحدث عندما تكون الشبكة معطّلة، أو المفتاح غير صالح (403)، أو عند بلوغ حد المعدل (429)؟ انمذج هذه الحالات بتعداد أخطاء مخصص باستخدام thiserror:

use thiserror::Error;

#[derive(Debug, Error)]
pub enum FinexlyError {
    #[error("HTTP request failed: {0}")]
    Http(#[from] reqwest::Error),

    #[error("API returned an unsuccessful response")]
    Unsuccessful,

    #[error("currency '{0}' was not present in the response")]
    MissingCurrency(String),
}

يعني السطر #[from] reqwest::Error أن أي عطل في reqwest — DNS أو TLS أو انتهاء المهلة أو حالة غير 2xx يلتقطها error_for_status() — يُحوَّل تلقائيًا إلى FinexlyError::Http عبر عامل ?. لهذا السبب يعمل كل .await? في latest() ببساطة.

يمكنك أيضًا فحص رمز الحالة صراحة عندما تريد سلوكًا مُصمَّمًا خصيصًا — على سبيل المثال، التراجع عند 429:

if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
    // sleep and retry, or fall back to a cached rate
}

للاطلاع على معالجة أعمق لإعادة المحاولات والتراجع واستراتيجية التخزين المؤقت، راجع دليلنا حول أفضل ممارسات التخزين المؤقت ومعالجة الأخطاء لواجهات برمجة تطبيقات العملات.

التحويل بين أي عملتين

الضرب في سعر واحد يعمل فقط عندما تكون عملة المصدر لديك هي الأساس. تحتاج التطبيقات الحقيقية إلى أزواج اعتباطية — EUR → JPY، وGBP → CAD — حيث لا يكون أيٌّ من الطرفين هو الأساس. الحيلة هي التوجيه عبر عملة الأساس: اقسم المبلغ على سعر المصدر للحصول على المكافئ بالأساس، ثم اضرب في سعر الهدف.

impl FinexlyClient {
    /// Convert `amount` from `from` to `to`, using `from` as the request base.
    pub async fn convert(
        &self,
        amount: f64,
        from: &str,
        to: &str,
    ) -> Result<f64, FinexlyError> {
        let data = self.latest(from, &[to]).await?;

        if !data.success {
            return Err(FinexlyError::Unsuccessful);
        }

        let rate = data
            .rates
            .get(to)
            .ok_or_else(|| FinexlyError::MissingCurrency(to.to_string()))?;

        Ok(amount * rate)
    }
}

هنا نطلب الأسعار ببساطة مع from كأساس، لذا فإن السعر المُرجَع هو بالفعل التحويل المباشر from → to. إذا خزّنت مؤقتًا عملة أساس واحدة (لنقل USD) واحتجت أسعارًا متقاطعة، فاحسبها في جانب العميل بدلاً من ذلك: amount / rates[from] * rates[to]. جرّبها من البداية إلى النهاية:

#[tokio::main]
async fn main() -> Result<(), FinexlyError> {
    let client = FinexlyClient::new(
        std::env::var("FINEXLY_API_KEY").expect("set FINEXLY_API_KEY"),
    );

    let usd = client.convert(250.0, "EUR", "USD").await?;
    println!("250 EUR = {usd:.2} USD");

    Ok(())
}

استخدام rust_decimal للمال

f64 مناسب لتحويلات العرض، لكنه النوع الخاطئ لتخزين الأرصدة، أو إصدار الفواتير، أو أي شيء يجب أن يتطابق حتى القرش. لا يمكن للفاصلة العائمة الثنائية تمثيل 0.1 بدقة، وتتراكم تلك الأخطاء. للمال، استخدم أرقامًا عشرية ذات فاصلة ثابتة:

use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use rust_decimal_macros::dec;

pub fn convert_decimal(amount: Decimal, rate: f64) -> Decimal {
    let rate = Decimal::from_f64(rate).unwrap_or_default();
    (amount * rate).round_dp(2)
}

// usage
let total = convert_decimal(dec!(1999.99), 0.9213);
println!("{total}"); // rounded to 2 decimal places

تحذير مهم واحد: ليست كل عملة لها وحدتان صغريان. JPY وKRW لهما صفر خانات عشرية؛ وبعض العملات مثل BHD وKWD لها ثلاث خانات. قرّب إلى العدد الصحيح من الخانات لكل عملة (وفق جدول الوحدات الصغرى ISO 4217) بدلاً من ترميز 2 بشكل ثابت. يعكس كلٌّ من محوّل العملات وتوثيق API لدينا هذه القواعد الخاصة بكل عملة.

التخزين المؤقت للبقاء ضمن الباقة المجانية

لا تتحرك أسعار العملات بشكل ملموس من ثانية إلى أخرى. بالنسبة لمعظم حالات استخدام العرض والدفع وإعداد التقارير، يكفي التحديث مرة كل ساعة — والتخزين المؤقت يُبقيك بأريحية ضمن الباقة المجانية مهما بلغ حجم حركة المرور لديك. أضف ذاكرة تخزين مؤقت بسيطة بمدة صلاحية (TTL) في الذاكرة محمية بـ Mutex:

use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};

struct CacheEntry {
    data: RatesResponse,
    fetched_at: Instant,
}

pub struct CachedClient {
    inner: FinexlyClient,
    ttl: Duration,
    cache: Mutex<HashMap<String, CacheEntry>>,
}

impl CachedClient {
    pub fn new(api_key: impl Into<String>) -> Self {
        Self {
            inner: FinexlyClient::new(api_key),
            ttl: Duration::from_secs(3600), // 1 hour
            cache: Mutex::new(HashMap::new()),
        }
    }

    pub async fn latest(&self, base: &str) -> Result<RatesResponse, FinexlyError> {
        {
            let cache = self.cache.lock().unwrap();
            if let Some(entry) = cache.get(base) {
                if entry.fetched_at.elapsed() < self.ttl {
                    return Ok(entry.data.clone());
                }
            }
        }

        let fresh = self.inner.latest(base, &[]).await?;
        let mut cache = self.cache.lock().unwrap();
        cache.insert(
            base.to_string(),
            CacheEntry { data: fresh.clone(), fetched_at: Instant::now() },
        );
        Ok(fresh)
    }
}

لكي تُترجم هذه الشيفرة، ستحتاج إلى #[derive(Clone)] على RatesResponse. بمدة صلاحية ساعة واحدة، تُكلّفك عملة أساس واحدة على الأكثر 24 طلبًا في اليوم بغض النظر عن عدد التحويلات التي يُجريها مستخدموك — وهو خطأ تقريب أمام باقة مجانية بحجم 1000 طلب. وإذا تجاوزتها، فإن خطط الأسعار تتوسع خطيًا. للاطلاع على أنماط مثل stale-while-revalidate والذواكر المؤقتة المشتركة عبر المثيلات، راجع دليل التخزين المؤقت ومعالجة الأخطاء.

جلب الأسعار التاريخية

تحتاج الفواتير المؤرَّخة بأثر رجعي، وإعداد التقارير، والرسوم البيانية إلى بيانات تاريخية. تُتيح Finexly نقطة نهاية تاريخية يمكنك استدعاؤها من العميل نفسه بإضافة أسلوب يصل إلى /v1/historical:

impl FinexlyClient {
    pub async fn historical(
        &self,
        date: &str, // "YYYY-MM-DD"
        base: &str,
        symbols: &[&str],
    ) -> Result<RatesResponse, FinexlyError> {
        let url = format!("{}/historical", self.base_url);
        let symbols_csv = symbols.join(",");

        let data = self
            .http
            .get(&url)
            .query(&[
                ("date", date),
                ("base", base),
                ("symbols", symbols_csv.as_str()),
                ("apikey", self.api_key.as_str()),
            ])
            .send()
            .await?
            .error_for_status()?
            .json::<RatesResponse>()
            .await?;

        Ok(data)
    }
}

شكل الاستجابة مطابق لنقطة نهاية أحدث الأسعار، لذا فإن كل شيفرة التحليل والتحويل الموجودة لديك تعمل ببساطة. راجع توثيق API للاطلاع على قائمة المعاملات الكاملة ونقطة النهاية /v1/timeseries لنطاقات التواريخ.

أخطاء شائعة يجب تجنّبها

  • إنشاء Client لكل طلب. أنشئ واحدًا، واستنسخه (فهو رخيص)، وأعِد استخدامه لتُبقي مجمّع الاتصالات دافئًا.
  • استخدام f64 للأرصدة المخزَّنة. مناسب للعرض، خاطئ للسجلات المحاسبية — استخدم rust_decimal لأي شيء يجب أن يتطابق.
  • ترميز خانتين عشريتين بشكل ثابت. JPY وKRW لهما صفر؛ وبعض العملات لها ثلاث. قرّب لكل عملة باستخدام الوحدات الصغرى ISO 4217.
  • تخطي فحص الحالة. إن إلغاء تسلسل جسم 403 أو 429 كبيانات أسعار يُنتج أخطاءً محيّرة. يعالج error_for_status() هذا في سطر واحد.
  • الجلب عند كل تحويل. خزّن مؤقتًا بمدة صلاحية (TTL) وطبّق منع الارتداد على مدخلات المستخدم كي لا تحرق حصتك.
  • تضمين مفتاح API الخاص بك في الالتزامات. اقرأه من البيئة أو من مدير أسرار، وليس أبدًا كسلسلة نصية حرفية في main.rs.

إذا كنت لا تزال تقارن بين المزوّدين، فإن مقارنتنا لواجهات برمجة تطبيقات العملات تُفصّل الدقة وتغطية العملات وحدود الباقة المجانية، كما يغطي دليل واجهة برمجة تطبيقات العملات المجانية الخيارات المجانية بمزيد من العمق.

الأسئلة الشائعة

ما هو أفضل عميل HTTP لاستدعاء واجهة برمجة تطبيقات عملات في Rust؟

reqwest هو الخيار المعياري لـ Rust غير المتزامن. فهو مبني على hyper وtokio، ويتولى تجميع الاتصالات وTLS وJSON جاهزًا من الصندوق، ويتلاءم بشكل طبيعي مع serde للاستجابات المُصنّفة بأنواع. فعّل ميزة json وأعِد استخدام مثيل Client واحد عبر تطبيقك.

كيف أحلّل استجابة JSON لأسعار الصرف في Rust؟

عرّف بنية تعكس الاستجابة واشتق عليها serde::Deserialize. انمذج حقل rates كـ HashMap<String, f64> لأنه خريطة مسطّحة من رمز العملة إلى السعر، ثم استدعِ .json::<RatesResponse>() على الاستجابة وreqwest يُلغي تسلسلها نيابة عنك.

هل ينبغي استخدام f64 أم Decimal لتحويل العملات في Rust؟

استخدم f64 لتحويلات العرض السريعة، لكن استخدم rust_decimal::Decimal لأي شيء تخزّنه أو تُطابقه — الأرصدة والفواتير والسجلات المحاسبية. لا يمكن للفاصلة العائمة الثنائية تمثيل الكسور العشرية بدقة، لذا تتراكم أخطاء التقريب. يمنحك rust_decimal دقة ذات فاصلة ثابتة.

كم مرة ينبغي أن أجلب أسعار الصرف في خدمة Rust؟

بالنسبة لمعظم حالات استخدام الدفع والعرض وإعداد التقارير، تكفي مرة كل ساعة — فالأسعار لا تتحرك خلال ساعة بما يكفي ليُهم. خزّن الاستجابة مؤقتًا بمدة صلاحية ساعة واحدة، وهو ما يُبقيك عند نحو 24 استدعاءً لكل عملة أساس في اليوم وبأريحية ضمن الباقة المجانية.

هل يمكنني الحصول على أسعار صرف تاريخية في Rust؟

نعم. أضف أسلوبًا يستدعي نقطة النهاية /v1/historical?date=YYYY-MM-DD على العميل نفسه. يتطابق شكل JSON مع نقطة نهاية أحدث الأسعار، لذا تعمل بناك الموجودة ومنطق التحويل دون تغيير. وهذا مفيد للفواتير المؤرَّخة بأثر رجعي، وإعداد التقارير، والرسوم البيانية.

ابدأ البناء

هل أنت مستعد لدمج أسعار الصرف الفورية في مشروع Rust الخاص بك؟ احصل على مفتاح Finexly API المجاني — دون بطاقة ائتمان. ابدأ بـ 1000 طلب مجاني شهريًا وارتقِ بالباقة مع نموّك. راجع توثيق API للاطلاع على المرجع الكامل لنقاط النهاية واستكشف خطط الأسعار عندما تكون مستعدًا لتوسيع ميزاتك متعددة العملات.

Vlado Grigirov

Senior Currency Markets Analyst & Financial Strategist

Vlado Grigirov is a senior currency markets analyst and financial strategist with over 14 years of experience in foreign exchange markets, cross-border finance, and currency risk management. He has wo...

View full profile →