Mercurial > lbo > hg > async-google-apis
changeset 9:bcd017d6c863
Make Drive API mostly work
author | Lewin Bormann <lbo@spheniscida.de> |
---|---|
date | Sat, 17 Oct 2020 15:55:51 +0200 |
parents | 755f08722795 |
children | 4b89ea16b73e |
files | generate/generate.py manual_demo/Cargo.lock manual_demo/Cargo.toml manual_demo/src/main.rs |
diffstat | 4 files changed, 207 insertions(+), 68 deletions(-) [+] |
line wrap: on
line diff
--- a/generate/generate.py Sat Oct 17 13:04:25 2020 +0200 +++ b/generate/generate.py Sat Oct 17 15:55:51 2020 +0200 @@ -8,6 +8,32 @@ from os import path +# General imports and error type. +RustHeader = ''' +use serde::{Deserialize, Serialize}; +use chrono::{DateTime, Utc}; +use anyhow::{Error, Result}; +use std::collections::HashMap; + +type TlsConnr = hyper_rustls::HttpsConnector<hyper::client::HttpConnector>; +type TlsClient = hyper::Client<TlsConnr, hyper::Body>; +type Authenticator = yup_oauth2::authenticator::Authenticator<TlsConnr>; + +#[derive(Debug, Clone)] +pub enum ApiError { + InputDataError(String), + HTTPError(hyper::StatusCode), +} + +impl std::error::Error for ApiError {} +impl std::fmt::Display for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(self, f) + } +} +''' + +# A struct for parameters or input/output API types. ResourceStructTmpl = ''' #[derive(Serialize, Deserialize, Debug, Clone, Default)] pub struct {{name}} { @@ -15,8 +41,10 @@ {{#comment}} // {{comment}} {{/comment}} + {{#attr}} {{{attr}}} - {{name}}: {{{typ}}}, + {{/attr}} + pub {{name}}: {{{typ}}}, {{/fields}} } ''' @@ -84,10 +112,16 @@ jsonname = pn cleaned_pn = snake_case(cleaned_pn) struct['fields'].append({ - 'name': cleaned_pn, - 'attr': '#[serde(rename = "{}")]'.format(jsonname), - 'typ': subtyp, - 'comment': comment + 'name': + cleaned_pn, + 'attr': + '#[serde(rename = "{}")]'.format(jsonname) + + '\n #[serde(skip_serializing_if = "Option::is_none")]' + if subtyp.startswith('Option') else '', + 'typ': + subtyp, + 'comment': + comment }) structs.extend(substructs) structs.append(struct) @@ -135,7 +169,42 @@ print(e) raise e -def resolve_parameters(string, paramsname='params', suffix='.unwrap()'): + +def scalar_type(jsont): + """Translate a scalar json type (for parameters) into Rust.""" + if jsont == 'boolean': + return 'bool' + elif jsont == 'string': + return 'String' + elif jsont == 'integer': + return 'i64' + raise Exception('unknown scalar type:', jsont) + + +def generate_parameter_types(resources): + """Generate parameter structs from the resources list.""" + structs = [] + for resourcename, resource in resources.items(): + for methodname, method in resource['methods'].items(): + print(resourcename, methodname) + struct = {'name': capitalize_first(resourcename) + capitalize_first(methodname) + 'Params', 'fields': []} + if 'parameters' in method: + for paramname, param in method['parameters'].items(): + struct['fields'].append({ + 'name': + snake_case(paramname), + 'typ': + optionalize(scalar_type(param['type']), not param.get('required', False)), + 'comment': + param.get('description', ''), + 'attr': + '#[serde(rename = "{}")]'.format(paramname), + }) + structs.append(struct) + return structs + + +def resolve_parameters(string, paramsname='params', suffix=''): """Returns a Rust syntax for formatting the given string with API parameters, and a list of (snake-case) API parameters that are used. """ pat = re.compile('\{(\w+)\}') @@ -144,39 +213,106 @@ format_params = ','.join(['{}={}.{}{}'.format(p, paramsname, sp, suffix) for (p, sp) in zip(params, snakeparams)]) return 'format!("{}", {})'.format(string, format_params), snakeparams + def generate_service(resource, methods, discdoc): + """Generate the code for all methods in a resource.""" service = capitalize_first(resource) parts = [] - parts.append('pub struct {}Service {{'.format(service)) - parts.append(' client: TlsClient,') - parts.append(' authenticator: Authenticator,') - parts.append('}') - parts.append('') + parts.append(''' +pub struct {}Service {{ + client: TlsClient, + authenticator: Authenticator, + scopes: Vec<String>, +}} +'''.format(service)) - parts.append('impl {}Service {{'.format(service)) + parts.append(''' +impl {service}Service {{ + /// Create a new {service}Service object. + pub fn new(client: TlsClient, auth: Authenticator) -> {service}Service {{ + {service}Service {{ client: client, authenticator: auth, scopes: vec![] }} + }} + /// Explicitly select which scopes should be requested for authorization. Otherwise, + /// a possibly too large scope will be requested. + pub fn set_scopes<S: AsRef<str>, T: AsRef<[S]>>(&mut self, scopes: T) {{ + self.scopes = scopes.as_ref().into_iter().map(|s| s.as_ref().to_string()).collect(); + }} +'''.format(service=service)) + + # Generate individual methods. for methodname, method in methods['methods'].items(): - params_name = service+capitalize_first(methodname)+'Params' + params_name = service + capitalize_first(methodname) + 'Params' + parameters = {p: snake_case(p) for p, pp in method.get('parameters', {}).items() if 'required' not in pp} in_type = method['request']['$ref'] if 'request' in method else '()' out_type = method['response']['$ref'] if 'response' in method else '()' is_upload = 'mediaUpload' in method + media_upload = method.get('mediaUpload', None) + if media_upload and 'simple' in media_upload['protocols']: + upload_path = media_upload['protocols']['simple']['path'] + else: + upload_path = '' + http_method = method['httpMethod'] - if is_upload: - parts.append(' fn {}(&mut self, params: {}, req: {}) -> Result<{}> {{'.format( - snake_case(methodname), params_name, in_type, out_type)) - else: - parts.append(' fn {}(&mut self, params: {}, req: {}, data: &hyper::body::Bytes) -> Result<{}> {{'.format( - snake_case(methodname), params_name, in_type, out_type)) + # TODO: Incorporate parameters into query! + for is_upload in set([False, is_upload]): + # TODO: Support multipart upload properly + if is_upload: + parts.append( + ' pub async fn {}_upload(&mut self, params: &{}, data: hyper::body::Bytes) -> Result<{}> {{'. + format(snake_case(methodname), params_name, out_type)) + else: + parts.append(' pub async fn {}(&mut self, params: &{}, req: &{}) -> Result<{}> {{'.format( + snake_case(methodname), params_name, in_type, out_type)) + + # Check parameters and format API path. + formatted_path, required_params = resolve_parameters(method['path']) + parts.append(' let relpath = {};'.format('"' + upload_path.lstrip('/') + + '"' if is_upload else formatted_path)) + parts.append(' let path = "{}".to_string() + &relpath;'.format( + discdoc['rootUrl'] if is_upload else discdoc['baseUrl'])) + parts.append(' let tok = self.authenticator.token(&self.scopes).await?;') + + if is_upload: + parts.append( + ' let mut url_params = format!("?uploadType=media&oauth_token={token}&fields=*", token=tok.as_str());' + ) + else: + parts.append(' let mut url_params = format!("?oauth_token={token}&fields=*", token=tok.as_str());') - formatted_path, required_params = resolve_parameters(method['path']) - for rp in required_params: - parts.append(' if params.{}.is_none() {{'.format(rp)) - parts.append(' return Err(Error::new(ApiError::InputDataError("Parameter {} is missing!".to_string())));'.format(rp)) - parts.append(' }') - parts.append(' let relpath = {};'.format(formatted_path)) - parts.append(' unimplemented!()') - parts.append(' }') + for p, snakeparam in parameters.items(): + parts.append(''' + if let Some(ref val) = ¶ms.{snake} {{ + url_params.push_str(&format!("&{p}={{}}", val)); + }}'''.format(p=p, snake=snakeparam)) + + parts.append(''' + let full_uri = path+&url_params; + println!("To: {{}}", full_uri); + let reqb = hyper::Request::builder().uri(full_uri).method("{method}");'''.format(method=http_method)) + if is_upload: + parts.append(''' + let reqb = reqb.header("Content-Length", data.len()); + let body = hyper::Body::from(data);''') + else: + parts.append(''' println!("Request: {}", serde_json::to_string(req)?);''') + if in_type != '()': + parts.append(''' let body = hyper::Body::from(serde_json::to_string(req)?);''') + else: + parts.append(''' let body = hyper::Body::from("");''') + + parts.append(''' let req = reqb.body(body)?; + let resp = self.client.request(req).await?; + if !resp.status().is_success() { + return Err(anyhow::Error::new(ApiError::HTTPError(resp.status()))); + } + let resp_body = hyper::body::to_bytes(resp.into_body()).await?; + let bodystr = String::from_utf8(resp_body.to_vec())?; + println!("Response: {}", bodystr); + let decoded = serde_json::from_str(&bodystr)?; + Ok(decoded) + }''') parts.append('}') parts.append('') @@ -192,48 +328,16 @@ for name, desc in schemas.items(): typ, substructs = type_of_property(name, desc) structs.extend(substructs) - for name, res in resources.items(): - for methodname, method in res['methods'].items(): - if 'parameters' not in method: - structs.append({ - 'name': '{}{}Params'.format(capitalize_first(name), capitalize_first(methodname)), - 'fields': [] - }) - else: - params = method['parameters'] - typ = {'type': 'object', 'properties': params} - typ, substructs = type_of_property( - '{}{}Params'.format(capitalize_first(name), capitalize_first(methodname)), typ) - structs.extend(substructs) + + # Generate parameter types. + structs.extend(generate_parameter_types(resources)) for resource, methods in resources.items(): services.append(generate_service(resource, methods, discdoc)) modname = (discdoc['id'] + '_types').replace(':', '_') with open(path.join('gen', modname + '.rs'), 'w') as f: - f.writelines([ - 'use serde::{Deserialize, Serialize};\n', - 'use chrono::{DateTime, Utc};\n', - 'use anyhow::{Error, Result};\n', - 'use std::collections::HashMap;\n', - ''' -type TlsConnr = hyper_rustls::HttpsConnector<hyper::client::HttpConnector>; -type TlsClient = hyper::Client<TlsConnr, hyper::Body>; -type Authenticator = yup_oauth2::authenticator::Authenticator<TlsConnr>; - -#[derive(Debug, Clone)] -pub enum ApiError { - InputDataError(String), -} - -impl std::error::Error for ApiError {} -impl std::fmt::Display for ApiError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Debug::fmt(self, f) - } -} -''', - ]) + f.write(RustHeader) for s in structs: for field in s['fields']: if field.get('comment', None):
--- a/manual_demo/Cargo.lock Sat Oct 17 13:04:25 2020 +0200 +++ b/manual_demo/Cargo.lock Sat Oct 17 15:55:51 2020 +0200 @@ -31,6 +31,12 @@ checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" [[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] name = "bitflags" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -406,6 +412,7 @@ version = "0.1.0" dependencies = [ "anyhow", + "base64 0.13.0", "chrono", "hyper", "hyper-rustls",
--- a/manual_demo/Cargo.toml Sat Oct 17 13:04:25 2020 +0200 +++ b/manual_demo/Cargo.toml Sat Oct 17 15:55:51 2020 +0200 @@ -16,3 +16,4 @@ tokio = { version = "~0.2", features = ["full"] } yup-oauth2 = "~4" anyhow = "~1.0" +base64 = "~0.13"
--- a/manual_demo/src/main.rs Sat Oct 17 13:04:25 2020 +0200 +++ b/manual_demo/src/main.rs Sat Oct 17 15:55:51 2020 +0200 @@ -56,6 +56,32 @@ println!("{:?}", about); } +async fn new_upload_file(cl: TlsClient, auth: Authenticator, f: &Path) { + let mut cl = drive::FilesService::new(cl, auth); + + let data = hyper::body::Bytes::from(fs::read(&f).unwrap()); + let mut params = drive::FilesCreateParams::default(); + params.include_permissions_for_view = Some("published".to_string()); + + let resp = cl.create_upload(¶ms, data).await.unwrap(); + + println!("{:?}", resp); + + let mut params = drive::FilesUpdateParams::default(); + println!("{:?}", params); + params.file_id = resp.id.clone().unwrap(); + params.include_permissions_for_view = Some("published".to_string()); + let mut file = resp; + file.name = Some("profilepic.jpg".to_string()); + file.original_filename = Some("profilepic.jpg".to_string()); + let update_resp = cl.update(¶ms, &file).await; + println!("{:?}", update_resp); + + let mut params = drive::FilesGetParams::default(); + params.file_id = file.id.clone().unwrap(); + println!("{:?}", cl.get(¶ms, &()).await.unwrap()); +} + async fn get_about(cl: &mut TlsClient, auth: &mut Authenticator) { let baseurl = "https://www.googleapis.com/drive/v3/"; let path = "about"; @@ -87,10 +113,11 @@ let mut cl = https_client(); //get_about(&mut cl, &mut auth).await; - upload_file(&mut cl, &mut auth, Path::new("pp.jpg")).await; + //upload_file(&mut cl, &mut auth, Path::new("pp.jpg")).await; + new_upload_file(cl, auth, Path::new("pp.jpg")).await; - match auth.token(scopes).await { - Ok(token) => println!("The token is {:?}", token), - Err(e) => println!("error: {:?}", e), - } + //match auth.token(scopes).await { + // Ok(token) => println!("The token is {:?}", token), + // Err(e) => println!("error: {:?}", e), + //} }