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}