use std::collections::BTreeMap;
use std::io::Write;
use schemars::schema_for;
use serde_json::Value;
use crate::api::{
ApiResponse, BookmarkedResult, DeletedResult, DmEvent, FollowingResult, LikedResult,
MutingResult, RetweetedResult, Tweet, UsageData, User,
};
use crate::cli::commands::auth::{AppStatusEntry, RedirectUriGetResponse, RedirectUriSetResponse};
use crate::error::{Result, XurlError};
use crate::output::OutputConfig;
use crate::skill_install::{InstallEnvelope, InstallMultiEnvelope};
struct SchemaEntry {
commands: &'static [&'static str],
type_name: &'static str,
}
const SCHEMA_ENTRIES: &[SchemaEntry] = &[
SchemaEntry {
commands: &["post", "reply", "quote", "read"],
type_name: "ApiResponse<Tweet>",
},
SchemaEntry {
commands: &["search", "timeline", "mentions", "bookmarks", "likes"],
type_name: "ApiResponse<Vec<Tweet>>",
},
SchemaEntry {
commands: &["whoami", "user"],
type_name: "ApiResponse<User>",
},
SchemaEntry {
commands: &["following", "followers"],
type_name: "ApiResponse<Vec<User>>",
},
SchemaEntry {
commands: &["like", "unlike"],
type_name: "ApiResponse<LikedResult>",
},
SchemaEntry {
commands: &["follow", "unfollow"],
type_name: "ApiResponse<FollowingResult>",
},
SchemaEntry {
commands: &["delete"],
type_name: "ApiResponse<DeletedResult>",
},
SchemaEntry {
commands: &["repost", "unrepost"],
type_name: "ApiResponse<RetweetedResult>",
},
SchemaEntry {
commands: &["bookmark", "unbookmark"],
type_name: "ApiResponse<BookmarkedResult>",
},
SchemaEntry {
commands: &["mute", "unmute"],
type_name: "ApiResponse<MutingResult>",
},
SchemaEntry {
commands: &["dm"],
type_name: "ApiResponse<DmEvent>",
},
SchemaEntry {
commands: &["dms"],
type_name: "ApiResponse<Vec<DmEvent>>",
},
SchemaEntry {
commands: &["usage"],
type_name: "ApiResponse<UsageData>",
},
SchemaEntry {
commands: &["auth-status", "auth-apps-list"],
type_name: "Vec<AppStatusEntry>",
},
SchemaEntry {
commands: &["redirect-uri-get"],
type_name: "RedirectUriGetResponse",
},
SchemaEntry {
commands: &["redirect-uri-set"],
type_name: "RedirectUriSetResponse",
},
SchemaEntry {
commands: &["skill-install"],
type_name: "InstallEnvelope",
},
SchemaEntry {
commands: &["skill-install-all"],
type_name: "InstallMultiEnvelope",
},
];
fn schema_for_command(command: &str) -> Result<Value> {
let schema = match command {
"post" | "reply" | "quote" | "read" => schema_for!(ApiResponse<Tweet>),
"search" | "timeline" | "mentions" | "bookmarks" | "likes" => {
schema_for!(ApiResponse<Vec<Tweet>>)
}
"whoami" | "user" => schema_for!(ApiResponse<User>),
"following" | "followers" => schema_for!(ApiResponse<Vec<User>>),
"like" | "unlike" => schema_for!(ApiResponse<LikedResult>),
"follow" | "unfollow" => schema_for!(ApiResponse<FollowingResult>),
"delete" => schema_for!(ApiResponse<DeletedResult>),
"repost" | "unrepost" => schema_for!(ApiResponse<RetweetedResult>),
"bookmark" | "unbookmark" => schema_for!(ApiResponse<BookmarkedResult>),
"mute" | "unmute" => schema_for!(ApiResponse<MutingResult>),
"dm" => schema_for!(ApiResponse<DmEvent>),
"dms" => schema_for!(ApiResponse<Vec<DmEvent>>),
"usage" => schema_for!(ApiResponse<UsageData>),
"auth-status" | "auth-apps-list" => schema_for!(Vec<AppStatusEntry>),
"redirect-uri-get" => schema_for!(RedirectUriGetResponse),
"redirect-uri-set" => schema_for!(RedirectUriSetResponse),
"skill-install" => schema_for!(InstallEnvelope),
"skill-install-all" => schema_for!(InstallMultiEnvelope),
"auth" | "media" | "completions" | "version" | "schema" => {
return Err(XurlError::validation(format!(
"schema not available for '{command}' (no typed response)"
)));
}
_ => {
let valid: Vec<&str> = SCHEMA_ENTRIES
.iter()
.flat_map(|e| e.commands.iter())
.copied()
.collect();
return Err(XurlError::validation(format!(
"unknown command '{command}'. Valid commands: {}",
valid.join(", ")
)));
}
};
Ok(serde_json::to_value(schema)?)
}
pub fn run_schema(
command: Option<&str>,
list: bool,
all: bool,
envelope: bool,
out: &OutputConfig,
stdout: &mut dyn Write,
) -> Result<()> {
if all {
return print_all_schemas(out, stdout);
}
if envelope || command == Some("envelope") {
let schema = crate::envelope::envelope_schema();
out.print_response(stdout, &schema);
return Ok(());
}
if list {
return print_schema_list(out, stdout);
}
match command {
Some(cmd) => {
let schema = schema_for_command(cmd)?;
out.print_response(stdout, &schema);
Ok(())
}
None => {
Err(XurlError::validation(
"usage: xr schema <COMMAND> | xr schema envelope | xr schema --list | xr schema --all",
))
}
}
}
fn print_schema_list(out: &OutputConfig, stdout: &mut dyn Write) -> Result<()> {
let mut entries: Vec<(&str, &str)> = Vec::new();
for entry in SCHEMA_ENTRIES {
for &cmd in entry.commands {
entries.push((cmd, entry.type_name));
}
}
entries.sort_by_key(|(cmd, _)| *cmd);
entries.push(("envelope", "Envelope"));
let max_cmd_len = entries.iter().map(|(cmd, _)| cmd.len()).max().unwrap_or(0);
for (cmd, type_name) in &entries {
out.print_message(stdout, &format!("{cmd:<max_cmd_len$} {type_name}"));
}
Ok(())
}
fn print_all_schemas(out: &OutputConfig, stdout: &mut dyn Write) -> Result<()> {
let mut all: BTreeMap<String, Value> = BTreeMap::new();
for entry in SCHEMA_ENTRIES {
for &cmd in entry.commands {
let schema = schema_for_command(cmd)?;
all.insert(cmd.to_string(), schema);
}
}
let value = serde_json::to_value(&all)?;
out.print_response(stdout, &value);
Ok(())
}