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) = &params.{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(&params, 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(&params, &file).await;
+    println!("{:?}", update_resp);
+
+    let mut params = drive::FilesGetParams::default();
+    params.file_id = file.id.clone().unwrap();
+    println!("{:?}", cl.get(&params, &()).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),
+    //}
 }