1use 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 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 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 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 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}