Skip to main content

orizn/
lib.rs

1//! Official Rust SDK for the Orizn Visa API.
2//!
3//! Check visa requirements for 39,585 passport-destination pairs in 15 languages.
4//!
5//! # Quick start
6//! ```rust,no_run
7//! use orizn::Orizn;
8//!
9//! #[tokio::main]
10//! async fn main() -> Result<(), orizn::Error> {
11//!     let client = Orizn::new();
12//!     let result = client.check("FRA", "JPN").await?;
13//!     println!("{}: {} days", result.requirement, result.visa_free_days.unwrap_or(0));
14//!     Ok(())
15//! }
16//! ```
17
18use reqwest::Client;
19use serde::{Deserialize, Serialize};
20use std::env;
21use std::sync::Once;
22
23#[derive(Debug, thiserror::Error)]
24pub enum Error {
25    #[error("HTTP error: {0}")]
26    Http(#[from] reqwest::Error),
27    #[error("API key required. Get one free at https://visa.orizn.app")]
28    AuthRequired,
29    #[error("Rate limit exceeded. Upgrade at https://visa.orizn.app")]
30    RateLimit,
31    #[error("No visa data for {passport} → {destination}")]
32    NotFound { passport: String, destination: String },
33    #[error("API error {status}: {message}")]
34    Api { status: u16, message: String },
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct VisaCheckResult {
39    pub passport: String,
40    pub destination: String,
41    pub requirement: String,
42    pub visa_free_days: Option<i32>,
43    pub visa_required: bool,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct CountryInfo {
48    pub currency: String,
49    pub language: String,
50    pub timezone: String,
51    pub capital: String,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct VisaData {
56    pub passport: String,
57    pub destination: String,
58    pub requirement: String,
59    pub visa_free_days: Option<i32>,
60    pub visa_required: bool,
61    pub description: String,
62    pub documents_required: Vec<String>,
63    pub process: Vec<String>,
64    pub tips: Vec<String>,
65    pub country_info: CountryInfo,
66    pub verified: bool,
67}
68
69#[derive(Debug, Deserialize)]
70struct ApiResponse<T> {
71    data: T,
72}
73
74pub struct Orizn {
75    client: Client,
76    api_key: Option<String>,
77    base_url: String,
78}
79
80impl Orizn {
81    /// Create a new client. Reads ORIZN_API_KEY from environment.
82    pub fn new() -> Self {
83        let api_key = env::var("ORIZN_API_KEY").ok();
84
85        static HINT: Once = Once::new();
86        if api_key.is_none() {
87            HINT.call_once(|| {
88                eprintln!("[orizn] No API key found. Free mode: quick checks only.");
89                eprintln!("[orizn] Get your free key → https://visa.orizn.app");
90                eprintln!("[orizn] Then set: ORIZN_API_KEY=orizn_visa_...");
91            });
92        }
93
94        Self {
95            client: Client::builder()
96                .timeout(std::time::Duration::from_secs(10))
97                .user_agent("orizn-rs/1.0.0")
98                .build()
99                .expect("Failed to build HTTP client"),
100            api_key,
101            base_url: "https://visa.orizn.app".to_string(),
102        }
103    }
104
105    /// Create with explicit API key.
106    pub fn with_key(api_key: impl Into<String>) -> Self {
107        let mut c = Self::new();
108        c.api_key = Some(api_key.into());
109        c
110    }
111
112    /// Quick visa check. No API key needed.
113    pub async fn check(&self, passport: &str, destination: &str) -> Result<VisaCheckResult, Error> {
114        let resp = self.client
115            .get(format!("{}/api/v1/visa/check", self.base_url))
116            .query(&[
117                ("passport", &passport.to_uppercase() as &str),
118                ("destination", &destination.to_uppercase() as &str),
119            ])
120            .send()
121            .await?;
122
123        self.handle_status(&resp, passport, destination)?;
124        let body: ApiResponse<VisaCheckResult> = resp.json().await?;
125        Ok(body.data)
126    }
127
128    /// Full visa details. Requires API key.
129    pub async fn get_visa(
130        &self,
131        passport: &str,
132        destination: &str,
133        lang: &str,
134    ) -> Result<VisaData, Error> {
135        let key = self.api_key.as_ref().ok_or(Error::AuthRequired)?;
136
137        let resp = self.client
138            .get(format!("{}/api/v1/visa", self.base_url))
139            .header("x-api-key", key)
140            .query(&[
141                ("passport", &passport.to_uppercase() as &str),
142                ("destination", &destination.to_uppercase() as &str),
143                ("lang", lang),
144            ])
145            .send()
146            .await?;
147
148        self.handle_status(&resp, passport, destination)?;
149        let body: ApiResponse<VisaData> = resp.json().await?;
150        Ok(body.data)
151    }
152
153    fn handle_status(
154        &self,
155        resp: &reqwest::Response,
156        passport: &str,
157        destination: &str,
158    ) -> Result<(), Error> {
159        match resp.status().as_u16() {
160            200..=299 => Ok(()),
161            401 | 403 => Err(Error::AuthRequired),
162            429 => Err(Error::RateLimit),
163            404 => Err(Error::NotFound {
164                passport: passport.to_string(),
165                destination: destination.to_string(),
166            }),
167            s => Err(Error::Api {
168                status: s,
169                message: format!("Unexpected status {s}"),
170            }),
171        }
172    }
173}
174
175impl Default for Orizn {
176    fn default() -> Self {
177        Self::new()
178    }
179}