use serde::{Deserialize, Serialize};
use crate::adapters::common::encode_path_segment;
use crate::error::{FinanceError, Result};
use super::super::build_client;
use super::super::models::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct AdditionalUnderlying {
#[serde(rename = "type")]
pub underlying_type: Option<String>,
pub underlying: Option<String>,
pub amount: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionsContract {
pub ticker: Option<String>,
pub underlying_ticker: Option<String>,
pub contract_type: Option<String>,
pub exercise_style: Option<String>,
pub expiration_date: Option<String>,
pub strike_price: Option<f64>,
pub cfi: Option<String>,
pub shares_per_contract: Option<u32>,
pub additional_underlyings: Option<Vec<AdditionalUnderlying>>,
pub primary_exchange: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct OptionsContractResponse {
pub request_id: Option<String>,
pub status: Option<String>,
pub results: Option<OptionsContract>,
}
pub async fn options_contracts(
params: &[(&str, &str)],
) -> Result<PaginatedResponse<OptionsContract>> {
let client = build_client()?;
let path = "/v3/reference/options/contracts";
client.get(path, params).await
}
pub async fn options_contract_details(ticker: &str) -> Result<OptionsContractResponse> {
let client = build_client()?;
let path = format!(
"/v3/reference/options/contracts/{}",
encode_path_segment(ticker)
);
let json = client.get_raw(&path, &[]).await?;
serde_json::from_value(json).map_err(|e| FinanceError::ResponseStructureError {
field: "options_contract_details".to_string(),
context: format!("Failed to parse options contract details response: {e}"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_options_contracts_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock("GET", "/v3/reference/options/contracts")
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("apiKey".into(), "test-key".into()),
mockito::Matcher::UrlEncoded("underlying_ticker".into(), "AAPL".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({
"request_id": "abc123",
"status": "OK",
"results": [
{
"ticker": "O:AAPL250117C00150000",
"underlying_ticker": "AAPL",
"contract_type": "call",
"exercise_style": "american",
"expiration_date": "2025-01-17",
"strike_price": 150.0,
"cfi": "OCASPS",
"shares_per_contract": 100,
"primary_exchange": "BATO"
},
{
"ticker": "O:AAPL250117P00150000",
"underlying_ticker": "AAPL",
"contract_type": "put",
"exercise_style": "american",
"expiration_date": "2025-01-17",
"strike_price": 150.0,
"cfi": "OPASPS",
"shares_per_contract": 100,
"primary_exchange": "BATO"
}
],
"resultsCount": 2
})
.to_string(),
)
.create_async()
.await;
let client = super::super::super::build_test_client(&server.url()).unwrap();
let resp: PaginatedResponse<OptionsContract> = client
.get(
"/v3/reference/options/contracts",
&[("underlying_ticker", "AAPL")],
)
.await
.unwrap();
let results = resp.results.unwrap();
assert_eq!(results.len(), 2);
assert_eq!(results[0].ticker.as_deref(), Some("O:AAPL250117C00150000"));
assert_eq!(results[0].contract_type.as_deref(), Some("call"));
assert!((results[0].strike_price.unwrap() - 150.0).abs() < 0.01);
assert_eq!(results[1].contract_type.as_deref(), Some("put"));
}
#[tokio::test]
async fn test_options_contract_details_mock() {
let mut server = mockito::Server::new_async().await;
let _mock = server
.mock(
"GET",
"/v3/reference/options/contracts/O:AAPL250117C00150000",
)
.match_query(mockito::Matcher::AllOf(vec![mockito::Matcher::UrlEncoded(
"apiKey".into(),
"test-key".into(),
)]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
serde_json::json!({
"request_id": "abc123",
"status": "OK",
"results": {
"ticker": "O:AAPL250117C00150000",
"underlying_ticker": "AAPL",
"contract_type": "call",
"exercise_style": "american",
"expiration_date": "2025-01-17",
"strike_price": 150.0,
"cfi": "OCASPS",
"shares_per_contract": 100,
"primary_exchange": "BATO",
"additional_underlyings": [
{ "type": "equity", "underlying": "AAPL", "amount": 100.0 }
]
}
})
.to_string(),
)
.create_async()
.await;
let client = super::super::super::build_test_client(&server.url()).unwrap();
let json = client
.get_raw("/v3/reference/options/contracts/O:AAPL250117C00150000", &[])
.await
.unwrap();
let resp: OptionsContractResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.status.as_deref(), Some("OK"));
let contract = resp.results.unwrap();
assert_eq!(contract.ticker.as_deref(), Some("O:AAPL250117C00150000"));
assert_eq!(contract.exercise_style.as_deref(), Some("american"));
assert_eq!(contract.shares_per_contract, Some(100));
let additional = contract.additional_underlyings.unwrap();
assert_eq!(additional.len(), 1);
assert_eq!(additional[0].underlying.as_deref(), Some("AAPL"));
}
}