Skip to main content

oas_document/
pricing.rs

1// OAS Spec §5.12 — Pricing & Economics
2//! Pricing and economics section for OAS Identity Documents.
3//!
4//! Describes "what I cost" — pricing model, currency, cost entries,
5//! billing endpoint, and free tier limits.
6
7use serde::{Deserialize, Serialize};
8
9use crate::visibility::Visibility;
10
11/// A cost entry describing pricing for a specific unit of work.
12///
13/// # Examples
14///
15/// ```
16/// use oas_document::pricing::CostEntry;
17///
18/// let cost = CostEntry {
19///     unit: "request".to_string(),
20///     amount: "0.001".to_string(),
21///     currency: "USD".to_string(),
22/// };
23/// ```
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "camelCase")]
26pub struct CostEntry {
27    /// What is being priced: `"request"`, `"token"`, `"minute"`,
28    /// `"task"`, `"subscription_monthly"`.
29    pub unit: String,
30
31    /// Amount as a decimal string (to avoid floating-point issues).
32    pub amount: String,
33
34    /// ISO 4217 currency code (e.g., `"USD"`, `"EUR"`, `"BTC"`, `"ETH"`).
35    pub currency: String,
36}
37
38/// Pricing and economics section of an OAS Identity Document.
39///
40/// # Examples
41///
42/// ```
43/// use oas_document::pricing::{PricingSection, CostEntry};
44/// use oas_document::visibility::Visibility;
45///
46/// let pricing = PricingSection {
47///     model: "per_request".to_string(),
48///     costs: vec![CostEntry {
49///         unit: "request".to_string(),
50///         amount: "0.001".to_string(),
51///         currency: "USD".to_string(),
52///     }],
53///     currencies_accepted: vec!["USD".to_string(), "ETH".to_string()],
54///     billing_endpoint: Some("https://api.acme.com/billing".to_string()),
55///     free_tier_limits: Some("100 requests/day".to_string()),
56///     billing_visibility: Visibility::Authenticated,
57/// };
58/// assert_eq!(pricing.model, "per_request");
59/// ```
60#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct PricingSection {
63    /// Pricing model: `"free"`, `"per_request"`, `"per_token"`,
64    /// `"subscription"`, `"negotiated"`, `"custom"`.
65    pub model: String,
66
67    /// Individual cost entries.
68    #[serde(default, skip_serializing_if = "Vec::is_empty")]
69    pub costs: Vec<CostEntry>,
70
71    /// Currencies accepted (ISO 4217 or crypto ticker).
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub currencies_accepted: Vec<String>,
74
75    /// Billing/payment endpoint URL.
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub billing_endpoint: Option<String>,
78
79    /// Free tier description (e.g., "100 requests/day", "1000 tokens/month").
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub free_tier_limits: Option<String>,
82
83    /// Visibility of billing details.
84    #[serde(default)]
85    pub billing_visibility: Visibility,
86}
87
88/// Standard pricing models.
89pub mod models {
90    pub const FREE: &str = "free";
91    pub const PER_REQUEST: &str = "per_request";
92    pub const PER_TOKEN: &str = "per_token";
93    pub const SUBSCRIPTION: &str = "subscription";
94    pub const NEGOTIATED: &str = "negotiated";
95    pub const CUSTOM: &str = "custom";
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn serde_round_trip() {
104        let pricing = PricingSection {
105            model: models::PER_REQUEST.to_string(),
106            costs: vec![CostEntry {
107                unit: "request".to_string(),
108                amount: "0.001".to_string(),
109                currency: "USD".to_string(),
110            }],
111            currencies_accepted: vec!["USD".to_string()],
112            billing_endpoint: Some("https://billing.example.com".to_string()),
113            free_tier_limits: Some("100 req/day".to_string()),
114            billing_visibility: Visibility::Authenticated,
115        };
116
117        let json = serde_json::to_string(&pricing).unwrap_or_default();
118        let parsed: PricingSection =
119            serde_json::from_str(&json).unwrap_or_else(|_| pricing.clone());
120        assert_eq!(pricing, parsed);
121    }
122
123    #[test]
124    fn free_model_minimal() {
125        let json = r#"{"model":"free"}"#;
126        let pricing: Result<PricingSection, _> = serde_json::from_str(json);
127        assert!(pricing.is_ok());
128        let p = pricing.unwrap_or_else(|_| PricingSection {
129            model: String::new(),
130            costs: vec![],
131            currencies_accepted: vec![],
132            billing_endpoint: None,
133            free_tier_limits: None,
134            billing_visibility: Visibility::Public,
135        });
136        assert_eq!(p.model, "free");
137    }
138}