Skip to content

Commit 9c034a2

Browse files
Updated at mer 11 feb 2026, 15:53:09, CET
1 parent ebfe74b commit 9c034a2

36 files changed

Lines changed: 551 additions & 159 deletions

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ oas-download:
5757
echo " WARNING: failed to download $$url — check the URL in oas/urls.txt" >&2; \
5858
errors=$$((errors + 1)); \
5959
fi; \
60-
done < oas/00-oas-list.txt; \
60+
done < oas/00-list.txt; \
6161
echo ""; \
6262
if [ $$errors -gt 0 ]; then \
6363
echo "Done with $$errors warning(s). Fix the URLs above in oas/urls.txt and re-run."; \
@@ -77,3 +77,13 @@ release:
7777

7878
install:
7979
@cargo install --path .
80+
81+
## =======
82+
## Testing
83+
## =======
84+
85+
test:
86+
@cargo test
87+
88+
test-commands:
89+
@bash tests/commands-test.sh

USAGE.md

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
# openapi CLI - Usage Guide
2+
3+
## Configuration
4+
5+
The CLI requires API tokens set as environment variables:
6+
7+
```bash
8+
export OPENAPI_TOKEN="your-production-token"
9+
export OPENAPI_SANDBOX_TOKEN="your-sandbox-token" # optional
10+
```
11+
12+
Check your configuration status:
13+
14+
```bash
15+
openapi info
16+
```
17+
18+
## Sandbox Mode
19+
20+
Use the `-S` (or `--sandbox`) flag to run against sandbox environments:
21+
22+
```bash
23+
openapi -S sms send --to "+391234567890" --message "Test"
24+
```
25+
26+
## Scope Aliases
27+
28+
When creating tokens with `openapi token create --scopes`, you can use **scope aliases** instead of writing full scope strings.
29+
30+
### How it works
31+
32+
Each API service is mapped to an alias name. When you use an alias, it automatically expands to all available scopes for that service.
33+
34+
| Alias | Service | Domain |
35+
|---|---|---|
36+
| `ai` | AI language models | ai.openapi.com |
37+
| `automotive` | Automotive data | automotive.openapi.com |
38+
| `cadastre` | Italian cadastral data | catasto.openapi.it |
39+
| `certified-email` | PEC / Legalmail | pec.openapi.it |
40+
| `chamber-of-commerce` | Chamber of Commerce | visurecamerali.openapi.it |
41+
| `company` | Company data | company.openapi.com |
42+
| `docuengine` | Official documents | docuengine.openapi.com |
43+
| `domains` | .it domain management | domains.altravia.com |
44+
| `esignature` | Electronic signature | esignature.openapi.com |
45+
| `exchange-rate` | Currency exchange rates | exchange.altravia.com |
46+
| `geocoding` | Geocoding | geocoding.openapi.it |
47+
| `invoice` | Electronic invoicing | invoice.openapi.com |
48+
| `massive-rem` | Massive REM | ws.pecmassiva.com |
49+
| `paying-bills` | Bills payment | ws.pagasubito.it |
50+
| `pdf` | HTML to PDF | pdf.openapi.it |
51+
| `postal-service` | Postal mail | ws.ufficiopostale.com |
52+
| `real-estate` | Real estate valuation | realestate.openapi.com |
53+
| `risk` | Risk reports and scoring | risk.openapi.com |
54+
| `sdi` | SDI electronic invoicing | sdi.openapi.it |
55+
| `sms` | SMS messaging | sms.openapi.com |
56+
| `time-stamping` | Time stamping | ws.marchetemporali.com |
57+
| `token` | OAuth token management | oauth.openapi.it |
58+
| `trust` | Trust verification | trust.openapi.com |
59+
| `visengine` | Official documents | visengine2.altravia.com |
60+
| `zip-codes` | Zip codes and municipalities | cap.openapi.it |
61+
62+
### Examples
63+
64+
Create a token with all SMS scopes:
65+
66+
```bash
67+
openapi token create --scopes "sms"
68+
```
69+
70+
Create a token with all SMS and Company scopes:
71+
72+
```bash
73+
openapi token create --scopes "sms,company"
74+
```
75+
76+
### Method filtering
77+
78+
Prefix an alias with an HTTP method to include only scopes for that method:
79+
80+
```bash
81+
# Only POST scopes for SMS
82+
openapi token create --scopes "post:sms"
83+
84+
# Only GET scopes for company
85+
openapi token create --scopes "get:company"
86+
87+
# Mix methods and full aliases
88+
openapi token create --scopes "post:sms,get:company,geocoding"
89+
```
90+
91+
Supported method prefixes: `GET`, `POST`, `PUT`, `PATCH`, `DELETE`.
92+
93+
### Case insensitive
94+
95+
Aliases and method prefixes are **case insensitive**:
96+
97+
```bash
98+
openapi token create --scopes "SMS"
99+
openapi token create --scopes "Post:Sms"
100+
openapi token create --scopes "POST:SMS"
101+
```
102+
103+
All of the above are equivalent.
104+
105+
### Literal scopes
106+
107+
If a term does not match any alias, it is passed through as a literal scope:
108+
109+
```bash
110+
# Mix aliases and literal scopes
111+
openapi token create --scopes "sms,GET:imprese.openapi.it/base"
112+
```
113+
114+
## Token Management
115+
116+
```bash
117+
# List active tokens
118+
openapi token list
119+
120+
# List all available scopes
121+
openapi token scopes
122+
123+
# Check credit
124+
openapi token credit
125+
126+
# Revoke a token
127+
openapi token revoke --token "token-id"
128+
```
129+
130+
## Commands
131+
132+
Run `openapi --help` to see all available commands, or `openapi <command> --help` for subcommand details.
File renamed without changes.

src/client.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ impl ApiClient {
1515
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
1616
headers.insert(
1717
AUTHORIZATION,
18-
HeaderValue::from_str(&format!("Bearer {}", config.api_key))?,
18+
HeaderValue::from_str(&format!("Bearer {}", config.token))?,
1919
);
2020

2121
let http = reqwest::Client::builder()
@@ -29,7 +29,7 @@ impl ApiClient {
2929
}
3030

3131
/// Pick the right base URL depending on sandbox mode
32-
pub fn base_url(&self, production: &str, sandbox: &str) -> &str {
32+
pub fn base_url<'a>(&self, production: &'a str, sandbox: &'a str) -> &'a str {
3333
if self.sandbox { sandbox } else { production }
3434
}
3535

@@ -53,16 +53,6 @@ impl ApiClient {
5353
Ok(body)
5454
}
5555

56-
pub async fn put(&self, url: &str, body: &Value) -> Result<Value> {
57-
let resp = self.http.put(url).json(body).send().await?;
58-
let status = resp.status();
59-
let body: Value = resp.json().await?;
60-
if !status.is_success() {
61-
anyhow::bail!("API error ({}): {}", status, body);
62-
}
63-
Ok(body)
64-
}
65-
6656
pub async fn delete(&self, url: &str) -> Result<Value> {
6757
let resp = self.http.delete(url).send().await?;
6858
let status = resp.status();

src/commands/info.rs

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,123 @@
11
use anyhow::Result;
2+
use reqwest::header::{HeaderValue, AUTHORIZATION, CONTENT_TYPE};
3+
use serde_json::Value;
24

3-
pub fn execute() -> Result<()> {
5+
use crate::scopes;
6+
7+
pub async fn execute() -> Result<()> {
48
println!("openapi-cli v{}", env!("CARGO_PKG_VERSION"));
59
println!();
610

7-
let username = std::env::var("OPENAPI_USERNAME").ok();
8-
let key = std::env::var("OPENAPI_KEY").ok();
9-
let sandbox_key = std::env::var("OPENAPI_SANDBOX_KEY").ok();
11+
let token = std::env::var("OPENAPI_TOKEN").ok();
12+
let sandbox_token = std::env::var("OPENAPI_SANDBOX_TOKEN").ok();
1013

11-
let has_username = username.as_ref().is_some_and(|v| !v.is_empty());
12-
let has_key = key.as_ref().is_some_and(|v| !v.is_empty());
13-
let has_sandbox_key = sandbox_key.as_ref().is_some_and(|v| !v.is_empty());
14+
let has_token = token.as_ref().is_some_and(|v| !v.is_empty());
15+
let has_sandbox_token = sandbox_token.as_ref().is_some_and(|v| !v.is_empty());
1416

1517
// Show variable status
1618
println!("Environment variables:");
17-
print_var_status("OPENAPI_USERNAME", has_username, &username);
18-
print_var_status("OPENAPI_KEY", has_key, &key);
19-
print_var_status("OPENAPI_SANDBOX_KEY", has_sandbox_key, &sandbox_key);
19+
print_var_status("OPENAPI_TOKEN", has_token, &token);
20+
print_var_status("OPENAPI_SANDBOX_TOKEN", has_sandbox_token, &sandbox_token);
2021
println!();
2122

2223
// Readiness assessment
23-
if has_username && has_key && has_sandbox_key {
24+
if has_token && has_sandbox_token {
2425
println!("Status: READY");
2526
println!(" Production and sandbox environments are both available.");
26-
} else if has_username && has_key {
27+
} else if has_token {
2728
println!("Status: READY (production only)");
2829
println!(" Production environment is available.");
29-
println!(" Set OPENAPI_SANDBOX_KEY to enable sandbox mode (-S).");
30-
} else if has_username && has_sandbox_key {
30+
println!(" Set OPENAPI_SANDBOX_TOKEN to enable sandbox mode (-S).");
31+
} else if has_sandbox_token {
3132
println!("Status: SANDBOX ONLY");
3233
println!(" Only the sandbox environment is available (use -S flag).");
33-
println!(" Set OPENAPI_KEY to enable production mode.");
34+
println!(" Set OPENAPI_TOKEN to enable production mode.");
3435
} else {
3536
println!("Status: NOT CONFIGURED");
3637
println!(" The CLI cannot operate. Set the required environment variables:");
37-
if !has_username {
38-
println!(" export OPENAPI_USERNAME=\"your-username\"");
38+
println!(" export OPENAPI_TOKEN=\"your-api-token\"");
39+
println!(" export OPENAPI_SANDBOX_TOKEN=\"your-sandbox-token\" (optional)");
40+
}
41+
42+
// Show token scopes if tokens are available
43+
if has_token {
44+
println!();
45+
println!("Production token scopes:");
46+
match fetch_token_scopes(token.as_ref().unwrap(), false).await {
47+
Ok(token_scopes) if token_scopes.is_empty() => {
48+
println!(" (no scopes)");
49+
}
50+
Ok(token_scopes) => {
51+
print!("{}", scopes::group_scopes_by_service(&token_scopes, false));
52+
}
53+
Err(e) => {
54+
println!(" Unable to retrieve ({})", e);
55+
}
3956
}
40-
if !has_key && !has_sandbox_key {
41-
println!(" export OPENAPI_KEY=\"your-api-key\"");
42-
println!(" export OPENAPI_SANDBOX_KEY=\"your-sandbox-key\" (optional)");
57+
}
58+
59+
if has_sandbox_token {
60+
println!();
61+
println!("Sandbox token scopes:");
62+
match fetch_token_scopes(sandbox_token.as_ref().unwrap(), true).await {
63+
Ok(token_scopes) if token_scopes.is_empty() => {
64+
println!(" (no scopes)");
65+
}
66+
Ok(token_scopes) => {
67+
print!("{}", scopes::group_scopes_by_service(&token_scopes, true));
68+
}
69+
Err(e) => {
70+
println!(" Unable to retrieve ({})", e);
71+
}
4372
}
4473
}
4574

4675
Ok(())
4776
}
4877

78+
async fn fetch_token_scopes(token: &str, sandbox: bool) -> Result<Vec<String>> {
79+
let base = if sandbox {
80+
"https://test.oauth.openapi.it"
81+
} else {
82+
"https://oauth.openapi.it"
83+
};
84+
let url = format!("{}/token/{}", base, token);
85+
86+
let mut headers = reqwest::header::HeaderMap::new();
87+
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
88+
headers.insert(
89+
AUTHORIZATION,
90+
HeaderValue::from_str(&format!("Bearer {}", token))?,
91+
);
92+
93+
let client = reqwest::Client::builder()
94+
.default_headers(headers)
95+
.build()?;
96+
97+
let resp = client.get(&url).send().await?;
98+
let status = resp.status();
99+
let body: Value = resp.json().await?;
100+
101+
if !status.is_success() {
102+
let msg = body["message"].as_str().unwrap_or("unknown error");
103+
anyhow::bail!("{}", msg);
104+
}
105+
106+
// Response: { "data": [{ "scopes": [...], "token": "...", "expire": ... }], "success": true }
107+
let scopes = body["data"]
108+
.as_array()
109+
.and_then(|arr| arr.first())
110+
.and_then(|entry| entry["scopes"].as_array())
111+
.map(|arr| {
112+
arr.iter()
113+
.filter_map(|v| v.as_str().map(String::from))
114+
.collect()
115+
})
116+
.unwrap_or_default();
117+
118+
Ok(scopes)
119+
}
120+
49121
fn print_var_status(name: &str, is_set: bool, value: &Option<String>) {
50122
if is_set {
51123
let v = value.as_ref().unwrap();
@@ -54,8 +126,8 @@ fn print_var_status(name: &str, is_set: bool, value: &Option<String>) {
54126
} else {
55127
"*".repeat(v.len())
56128
};
57-
println!(" {:<25} SET ({})", name, masked);
129+
println!(" {:<30} SET ({})", name, masked);
58130
} else {
59-
println!(" {:<25} NOT SET", name);
131+
println!(" {:<30} NOT SET", name);
60132
}
61133
}

0 commit comments

Comments
 (0)