Rust で OAuth のアクセストークンを使ってスプレッドシートの範囲の値の取得と更新をするプログラムを書いたので、簡単に載せておく。
↓は JSON レスポンスを受け取る用の型(QuickType で生成した)
spreadsheets.rs
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCellInput {
pub value_input_option: String,
pub data: Vec<ValueRange>,
pub include_values_in_response: bool,
pub response_value_render_option: String,
pub response_date_time_render_option: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValueRange {
pub range: String,
pub major_dimension: String,
pub values: Vec<Vec<Value>>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Value {
Bool(bool),
Integer(i64),
String(String),
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Bool(b) => {
write!(f, "{}", b)
},
Self::Integer(i) => {
write!(f, "{}", i)
},
Self::String(s) => {
write!(f, "{}", s)
},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCellResult {
pub spreadsheet_id: String,
pub updated_range: String,
pub updated_rows: i64,
pub updated_columns: i64,
pub updated_cells: i64,
pub updated_data: Option<ValueRange>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCellBatchResult {
pub spreadsheet_id: String,
pub total_updated_rows: i32,
pub total_updated_columns: i32,
pub total_updated_cells: i32,
pub total_updated_sheets: i32,
pub responses: Vec<Response>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Response {
pub spreadsheet_id: String,
pub updated_range: String,
pub updated_rows: i32,
pub updated_columns: i32,
pub updated_cells: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SSError {
pub code: i32,
pub message: String,
pub status: String,
}
impl fmt::Display for SSError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "code={}, message={}, status={}", self.code, self.message, self.status)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RequestError {
pub error: SSError,
}
impl fmt::Display for RequestError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.error)
}
}
impl std::error::Error for RequestError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetCellBatchResult {
pub spreadsheet_id: String,
pub value_ranges: Vec<ValueRange>
}
セルレンジの取得は以下
spreadsheets.rs
pub async fn get_cell(token: &str, sheet_id: &str, range: &str) -> Result<ValueRange, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()?;
let result = client.get(format!("https://sheets.googleapis.com/v4/spreadsheets/{}/values/{}?majorDimension=ROWS&valueRenderOption=FORMATTED_VALUE&dateTimeRenderOption=FORMATTED_STRING", sheet_id, range))
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
.send()
.await?;
match result.status() {
StatusCode::OK => {
Ok(result.json().await?)
},
_ => {
Err(Box::new(result.json::<RequestError>().await?))
},
}
}
pub async fn get_cell_batch(token: &str, sheet_id: &str, range: &str) -> Result<GetCellBatchResult, Box<dyn std::error::Error + Send + Sync>> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()?;
let result = client.get(format!("https://sheets.googleapis.com/v4/spreadsheets/{}/values:batchGet?ranges={}&majorDimension=ROWS&valueRenderOption=FORMATTED_VALUE&dateTimeRenderOption=FORMATTED_STRING", sheet_id, range))
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
.send()
.await?;
match result.status() {
StatusCode::OK => {
Ok(result.json().await?)
},
_ => {
Err(Box::new(result.json::<RequestError>().await?))
},
}
}
更新は以下
spreadsheets.rs
pub async fn update_cell(token: &str, sheet_id: &str, range: &str, values: Vec<Value>) -> Result<UpdateCellResult, Box<dyn std::error::Error + Send + Sync>> {
let input = ValueRange {
range: range.to_string(),
major_dimension: "ROWS".to_string(),
values: vec![values]
};
let input_json = serde_json::to_string(&input)?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()?;
let result = client.put(format!("https://sheets.googleapis.com/v4/spreadsheets/{}/values/{}?valueInputOption=USER_ENTERED&includeValuesInResponse=false&responseValueRenderOption=FORMATTED_VALUE&responseDateTimeRenderOption=FORMATTED_STRING", sheet_id, range))
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
.body(input_json)
.send()
.await?;
match result.status() {
StatusCode::OK => {
Ok(result.json().await?)
},
_ => {
Err(Box::new(result.json::<RequestError>().await?))
},
}
}
pub async fn update_cell_batch(token: &str, sheet_id: &str, range: &str, values: Vec<Value>) -> Result<UpdateCellBatchResult, Box<dyn std::error::Error + Send + Sync>> {
let input = UpdateCellInput {
value_input_option: "USER_ENTERED".to_string(),
data: vec![ValueRange {
range: range.to_string(),
major_dimension: "ROWS".to_string(),
values: vec![values],
}],
include_values_in_response: false,
response_value_render_option: "FORMATTED_VALUE".to_string(),
response_date_time_render_option: "FORMATTED_STRING".to_string(),
};
let input_json = serde_json::to_string(&input)?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()?;
let result = client.post(format!("https://sheets.googleapis.com/v4/spreadsheets/{}/values:batchUpdate", sheet_id))
.header(reqwest::header::AUTHORIZATION, format!("Bearer {}", token))
.body(input_json)
.send()
.await?;
match result.status() {
StatusCode::OK => {
Ok(result.json().await?)
},
_ => {
Err(Box::new(result.json::<RequestError>().await?))
},
}
}
Google OAuth のアクセストークンの更新は以下
oauth.rs
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RefreshResult {
pub access_token: String,
pub expires_in: u32,
pub scope: String,
pub token_type: String,
}
pub async fn refresh_access_token(refresh_token: &str, client_id: &str, client_secret: &str, redirect_uri: &str) -> Result<RefreshResult, Box<dyn std::error::Error + Send + Sync>> {
let mut params = HashMap::new();
params.insert("refresh_token", refresh_token);
params.insert("client_id", client_id);
params.insert("client_secret", client_secret);
params.insert("grant_type", "refresh_token");
params.insert("redirect_uri", redirect_uri);
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.build()?;
let result = client.post("https://www.googleapis.com/oauth2/v4/token")
.form(¶ms)
.send()
.await?
.json::<RefreshResult>()
.await?;
Ok(result)
}