changeset 58:e6204806a74e

Use multipart API in generated code
author Lewin Bormann <lbo@spheniscida.de>
date Mon, 19 Oct 2020 19:29:35 +0200
parents f6150c39a944
children de733a7cd1a7
files README.md generate/generate.py generate/templates.py
diffstat 3 files changed, 36 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/README.md	Mon Oct 19 19:29:18 2020 +0200
+++ b/README.md	Mon Oct 19 19:29:35 2020 +0200
@@ -43,10 +43,4 @@
 * Don't always fetch all fields. Currently, the parameter `&fields=*` is sent
 with every request, which guarantees a full response, but not the best
 performance.
-* Multipart uploads are not yet supported. As a crutch, uploadable API endpoints
-are defined using two methods: `method()` and `method_upload()`, where
-`method_upload()` only uploads data, and `method()` only works with metadata.
-This works at least for my favorite API, the Google Drive API (v3). @Byron has a
-simple implementation of multipart HTTP in his excellent
-[Byron/google-apis-rs](https://github.com/Byron/google-apis-rs) crate; something
-similar may be useful here.
+* No resumable uploads -- even big files need to be held in memory.
--- a/generate/generate.py	Mon Oct 19 19:29:18 2020 +0200
+++ b/generate/generate.py	Mon Oct 19 19:29:35 2020 +0200
@@ -57,7 +57,7 @@
         second element is a comment detailing the use of the field. The list of
         dicts returned as second element are any structs that need to be separately
         implemented and that the generated struct (if it was a struct) depends
-        on. The dict contains elements as expected by templates.ResourceStructTmpl.
+        on. The dict contains elements as expected by templates.SchemaStructTmpl.
     """
     typ = ''
     comment = ''
@@ -70,10 +70,14 @@
             # There are two types of objects: those with `properties` are translated into a Rust struct,
             # and those with `additionalProperties` into a HashMap<String, ...>.
 
-            # Structs are represented as dicts that can be used to render the ResourceStructTmpl.
+            # Structs are represented as dicts that can be used to render the SchemaStructTmpl.
             if 'properties' in schema:
                 typ = name
-                struct = {'name': name, 'fields': []}
+                struct = {
+                        'name': name,
+                        'description': schema.get('description', ''),
+                        'fields': []
+                }
                 for pn, pp in schema['properties'].items():
                     subtyp, substructs = parse_schema_types(name + capitalize_first(pn), pp, optional=True)
                     if type(subtyp) is tuple:
@@ -181,7 +185,11 @@
             param_type_name = capitalize_first(super_name) + capitalize_first(resourcename) + capitalize_first(
                 methodname) + 'Params'
             print("processed:", resourcename, methodname, param_type_name)
-            struct = {'name': param_type_name, 'fields': []}
+            struct = {
+                    'name': param_type_name,
+                    'description': 'Parameters for the `{}.{}` method.'.format(resourcename, methodname),
+                    'fields': []
+            }
             # Build struct dict for rendering.
             if 'parameters' in method:
                 for paramname, param in method['parameters'].items():
@@ -192,7 +200,7 @@
                         'comment': desc,
                         'attr': '#[serde(rename = "{}")]'.format(paramname),
                     })
-            frags.append(chevron.render(ResourceStructTmpl, struct))
+            frags.append(chevron.render(SchemaStructTmpl, struct))
         # Generate parameter types for subresources.
         frags.extend(generate_params_structs(resource.get('resources', {}), super_name=resourcename))
     return frags
@@ -280,7 +288,6 @@
             if in_type == '()':
                 data_download.pop('in_type')
             method_fragments.append(chevron.render(DownloadMethodTmpl, data_download))
-
         else:
             data_normal = {
                 'name': snake_case(methodname),
@@ -307,10 +314,12 @@
                 data_normal.pop('in_type')
             method_fragments.append(chevron.render(NormalMethodTmpl, data_normal))
 
+            # We generate an additional implementation with the option of uploading data.
             if is_upload:
                 data_upload = {
                     'name': snake_case(methodname),
                     'param_type': params_type_name,
+                    'in_type': in_type,
                     'out_type': out_type,
                     'base_path': discdoc['rootUrl'],
                     'rel_path_expr': '"' + upload_path.lstrip('/') + '"',
@@ -389,7 +398,7 @@
                     field['comment'] = field['comment'].replace('\n', ' ')
             if not s['name']:
                 print('WARN', s)
-            f.write(chevron.render(ResourceStructTmpl, s))
+            f.write(chevron.render(SchemaStructTmpl, s))
         # Render *Params structs.
         for pt in parameter_types:
             f.write(pt)
--- a/generate/templates.py	Mon Oct 19 19:29:18 2020 +0200
+++ b/generate/templates.py	Mon Oct 19 19:29:35 2020 +0200
@@ -43,7 +43,8 @@
 # Dict contents --
 # name
 # fields: [{name, comment, attr, typ}]
-ResourceStructTmpl = '''
+SchemaStructTmpl = '''
+/// {{{description}}}
 #[derive(Serialize, Deserialize, Debug, Clone, Default)]
 pub struct {{{name}}} {
 {{#fields}}
@@ -59,10 +60,12 @@
 '''
 
 # Dict contents --
-# service (e.g. Files)
-# methods: [{text}]
+#
+# api, service (names: e.g. Files)
+# methods: [{text}] (the method implementations as {'text': ...} dicts)
 # name (API name)
 ServiceImplementationTmpl = '''
+/// The {{{name}}} {{{service}}} service represents the {{{service}}} resource.
 pub struct {{{service}}}Service {
     client: TlsClient,
     authenticator: Box<dyn 'static + std::ops::Deref<Target=Authenticator>>,
@@ -140,8 +143,10 @@
 # http_method
 UploadMethodTmpl = '''
 /// {{{description}}}
+///
+/// This method is a variant of `{{{name}}}()`, taking data for upload.
 pub async fn {{{name}}}_upload(
-    &mut self, params: &{{{param_type}}}, data: hyper::body::Bytes) -> Result<{{out_type}}> {
+    &mut self, params: &{{{param_type}}}, {{#in_type}}req: &{{{in_type}}},{{/in_type}} data: hyper::body::Bytes) -> Result<{{out_type}}> {
     let rel_path = {{{rel_path_expr}}};
     let path = "{{{base_path}}}".to_string() + &rel_path;
 
@@ -153,7 +158,7 @@
     } else {
         tok = self.authenticator.token(&self.scopes).await?;
     }
-    let mut url_params = format!("?uploadType=media&oauth_token={token}&fields=*", token=tok.as_str());
+    let mut url_params = format!("?uploadType=multipart&oauth_token={token}&fields=*", token=tok.as_str());
 
     {{#params}}
     if let Some(ref val) = &params.{{{snake_param}}} {
@@ -167,7 +172,12 @@
     {{/required_params}}
 
     let full_uri = path + &url_params;
-    do_upload(&self.client, &full_uri, &[], "{{{http_method}}}", data).await
+    let opt_request: Option<EmptyRequest> = None;
+    {{#in_type}}
+    let opt_request = Some(req);
+    {{/in_type}}
+
+    do_upload_multipart(&self.client, &full_uri, &[], "{{{http_method}}}", opt_request, data).await
   }
 '''
 
@@ -178,8 +188,10 @@
 # http_method
 DownloadMethodTmpl = '''
 /// {{{description}}}
+///
+/// This method downloads data.
 pub async fn {{{name}}}(
-    &mut self, params: &{{{param_type}}}, {{#in_type}}req: {{{in_type}}},{{/in_type}} dst: &mut dyn std::io::Write)
+    &mut self, params: &{{{param_type}}}, {{#in_type}}req: &{{{in_type}}},{{/in_type}} dst: &mut dyn std::io::Write)
     -> Result<{{out_type}}> {
 
     let rel_path = {{{rel_path_expr}}};