Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5b4f18d

Browse files
committed
fix(api+cli): improved scope handling; fix CLI
* in APIs, scopes will now be per-method, and if no scope is given, we will assume only the API key has to be set. Previously there was a wild mix between globally mentioned scopes and method scopes. * assure CLI generation works so far, for all avaialable APIs Related to #48
1 parent 6d3bbce commit 5b4f18d

5 files changed

Lines changed: 109 additions & 28 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Click the image below to see the playlist with all project related content:
3030

3131
Each episode sums up one major step in project development:
3232

33-
* [Episode 1](http://youtu.be/2U3SpepKaBE): How to write 78 APIs in 5s
33+
* [Episode 1](http://youtu.be/2U3SpepKaBE): How to write 78 APIs in 5 seconds
3434

3535
# Build Instructions
3636

src/mako/api/lib/mbuild.mako

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
indent_by, to_rust_type, rnd_arg_val_for_type, extract_parts, mb_type_params_s,
99
hub_type_params_s, method_media_params, enclose_in, mb_type_bounds, method_response,
1010
CALL_BUILDER_MARKERT_TRAIT, pass_through, markdown_rust_block, parts_from_params,
11-
DELEGATE_PROPERTY_NAME, struct_type_bounds_s, supports_scopes, scope_url_to_variant,
11+
DELEGATE_PROPERTY_NAME, struct_type_bounds_s, scope_url_to_variant,
1212
re_find_replacements, ADD_PARAM_FN, ADD_PARAM_MEDIA_EXAMPLE, upload_action_fn, METHODS_RESOURCE,
1313
method_name_to_variant, unique_type_name, size_to_bytes, method_default_scope,
1414
is_repeated_property)
@@ -119,7 +119,7 @@ pub struct ${ThisType}
119119
% endfor
120120
## A generic map for additinal parameters. Sometimes you can set some that are documented online only
121121
${api.properties.params}: HashMap<String, String>,
122-
% if supports_scopes(auth):
122+
% if method_default_scope(m):
123123
## We need the scopes sorted, to not unnecessarily query new tokens
124124
${api.properties.scopes}: BTreeMap<String, ()>
125125
% endif
@@ -156,7 +156,7 @@ ${self._setter_fn(resource, method, m, p, part_prop, ThisType, c)}\
156156
self
157157
}
158158
159-
% if supports_scopes(auth):
159+
% if method_default_scope(m):
160160
/// Identifies the authorization scope for the method you are building.
161161
///
162162
/// Use this method to actively specify which scope should be used, instead the default `Scope` variant
@@ -428,9 +428,7 @@ match result {
428428
delegate_finish = 'dlg.finished'
429429
auth_call = 'self.hub.auth.borrow_mut()'
430430
431-
if supports_scopes(auth):
432-
default_scope = method_default_scope(m)
433-
# end handle default scope
431+
default_scope = method_default_scope(m)
434432
435433
# s = '{foo}' -> ('{foo}', 'foo') -> (find_this, replace_with)
436434
seen = set()
@@ -573,7 +571,7 @@ else {
573571
% else:
574572
let mut url = "${baseUrl}${m.path}".to_string();
575573
% endif
576-
% if not supports_scopes(auth):
574+
% if not default_scope:
577575
<%
578576
assert 'key' in parameters, "Expected 'key' parameter if there are no scopes"
579577
%>
@@ -657,7 +655,7 @@ else {
657655
% endif
658656
659657
loop {
660-
% if supports_scopes(auth):
658+
% if default_scope:
661659
let mut token = ${auth_call}.token(self.${api.properties.scopes}.keys());
662660
if token.is_none() {
663661
token = dlg.token();
@@ -706,7 +704,7 @@ else {
706704
let mut client = &mut *self.hub.client.borrow_mut();
707705
let mut req = client.borrow_mut().request(${method_name_to_variant(m.httpMethod)}, url.as_ref())
708706
.header(UserAgent(self.hub._user_agent.clone()))\
709-
% if supports_scopes(auth):
707+
% if default_scope:
710708
711709
.header(auth_header.clone())\
712710
% endif

src/mako/cli/docs/commands.md.mako

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from util import (hash_comment, new_context, method_default_scope, indent_all_but_first_by, is_repeated_property)
44
from cli import (subcommand_md_filename, new_method_context, SPLIT_START, SPLIT_END, pretty, SCOPE_FLAG,
55
mangle_subcommand, is_request_value_property, FIELD_SEP, PARAM_FLAG, UPLOAD_FLAG, docopt_mode,
6-
FILE_ARG, MIME_ARG, OUT_ARG, OUTPUT_FLAG)
6+
FILE_ARG, MIME_ARG, OUT_ARG, OUTPUT_FLAG, to_cli_schema, cli_schema_to_yaml)
77
88
from copy import deepcopy
99
1010
escape_html = lambda n: n.replace('>', r'\>')
11+
12+
NO_DESC = 'No description provided.'
1113
%>\
1214
<%
1315
c = new_context(schemas, resources, context.get('methods'))
@@ -18,7 +20,7 @@
1820
mc = new_method_context(resource, method, c)
1921
%>\
2022
${SPLIT_START} ${subcommand_md_filename(resource, method)}
21-
% if mc.m.description:
23+
% if 'description' in mc.m:
2224
${mc.m.description}
2325
% endif # show method description
2426
% if mc.m.get('scopes'):
@@ -48,26 +50,21 @@ You can set the scope for this method like this: `${util.program_name()} --${SCO
4850
# Required Scalar ${len(rprops) > 1 and 'Arguments' or 'Argument'}
4951
% for p in rprops:
5052
* **<${mangle_subcommand(p.name)}\>**
51-
- ${p.get('description') or 'No description provided' | indent_all_but_first_by(2)}
53+
- ${p.get('description') or NO_DESC | indent_all_but_first_by(2)}
5254
% endfor # each required property (which is not the request value)
5355
% endif # have required properties
5456
% if mc.request_value:
57+
<%
58+
request_cli_schema = to_cli_schema(c, mc.request_value)
59+
%>\
5560
# Required Request Value
5661

5762
The request value is a data-structure with various fields. Each field may be a simple scalar or another data-structure.
5863
In the latter case it is advised to set the field-cursor to the data-structure's field to specify values more concisely.
5964

6065
For example, a structure like this:
6166
```
62-
"scalar_int": 5,
63-
"struct": {
64-
"scalar_float": 2.4
65-
"sub_struct": {
66-
"strings": ["baz", "bar"],
67-
"mapping": HashMap,
68-
}
69-
}
70-
"scalar_str": "foo",
67+
${cli_schema_to_yaml(request_cli_schema)}
7168
```
7269

7370
can be set completely with the following arguments. Note how the cursor position is adjusted the respective fields:
@@ -91,7 +88,7 @@ This method supports the upload of data, using the following protocol${len(mc.me
9188

9289
* **-${UPLOAD_FLAG} ${docopt_mode(protocols)} ${escape_html(FILE_ARG)} ${escape_html(MIME_ARG)}**
9390
% for mp in mc.media_params:
94-
- **${mp.protocol}** - ${mp.description.split('\n')[0]}
91+
- **${mp.protocol}** - ${mp.get('description', NO_DESC).split('\n')[0]}
9592
% endfor # each media param
9693
- **${escape_html(FILE_ARG)}**
9794
+ Path to file to upload. It must be seekable.
@@ -153,5 +150,5 @@ ${SPLIT_END}
153150

154151
<%def name="_md_property(p)">\
155152
* **-${PARAM_FLAG} ${mangle_subcommand(p.name)}=${p.type}**
156-
- ${p.get('description') or "No description provided" | indent_all_but_first_by(2)}
153+
- ${p.get('description') or NO_DESC | indent_all_but_first_by(2)}
157154
</%def>

src/mako/cli/lib/cli.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import re
55
import collections
6+
from copy import deepcopy
67

78
SPLIT_START = '>>>>>>>'
89
SPLIT_END = '<<<<<<<'
@@ -22,12 +23,19 @@
2223

2324
CONFIG_DIR = '~/.google-service-cli'
2425

26+
POD_TYPES = set(('boolean', 'integer', 'number', 'uint32', 'double', 'float', 'int32', 'int64', 'uint64', 'string'))
27+
2528
re_splitters = re.compile(r"%s ([\w\-\.]+)\n(.*?)\n%s" % (SPLIT_START, SPLIT_END), re.MULTILINE|re.DOTALL)
2629

2730
MethodContext = collections.namedtuple('MethodContext', ['m', 'response_schema', 'params', 'request_value',
2831
'media_params' ,'required_props', 'optional_props',
2932
'part_prop'])
3033

34+
CTYPE_POD = 'pod'
35+
CTYPE_ARRAY = 'list'
36+
CTYPE_MAP = 'map'
37+
SchemaEntry = collections.namedtuple('SchemaEntry', ['container_type', 'actual_property', 'property'])
38+
3139
def new_method_context(resource, method, c):
3240
m = c.fqan_map[util.to_fqan(c.rtc_map[resource], resource, method)]
3341
response_schema = util.method_response(c, m)
@@ -46,6 +54,7 @@ def pretty(n):
4654
def is_request_value_property(mc, p):
4755
return mc.request_value and mc.request_value.id == p.get(util.TREF)
4856

57+
4958
# transform name to be a suitable subcommand
5059
def mangle_subcommand(name):
5160
return util.camel_to_under(name).replace('_', '-').replace('.', '-')
@@ -55,12 +64,86 @@ def mangle_subcommand(name):
5564
def subcommand_md_filename(resource, method):
5665
return mangle_subcommand(resource) + '_' + mangle_subcommand(method) + '.md'
5766

67+
5868
def docopt_mode(protocols):
5969
mode = '|'.join(protocols)
6070
if len(protocols) > 1:
6171
mode = '(%s)' % mode
6272
return mode
6373

74+
75+
# Return schema' with fields dict: { 'field1' : SchemaField(...), 'SubSchema': schema' }
76+
def to_cli_schema(c, schema):
77+
res = deepcopy(schema)
78+
fd = dict()
79+
res['fields'] = fd
80+
81+
# util.nested_type_name
82+
properties = schema.get('properties', dict())
83+
if not properties and 'variant' in schema and 'map' in schema.variant:
84+
for e in schema.variant.map:
85+
assert util.TREF in e
86+
properties[e.type_value] = e
87+
# end handle enumerations
88+
89+
for pn, p in properties.iteritems():
90+
def set_nested_schema(ns):
91+
if ns.fields:
92+
fd[pn] = ns
93+
# end utility
94+
95+
def dup_property():
96+
pc = deepcopy(p)
97+
if 'type' in pc and pc.type == 'string' and 'Count' in pn:
98+
pc.type = 'int64'
99+
return pc
100+
# end
101+
102+
if util.TREF in p:
103+
if p[util.TREF] != schema.id: # prevent recursion (in case of self-referential schemas)
104+
set_nested_schema(to_cli_schema(c, c.schemas[p[util.TREF]]))
105+
elif p.type == 'array' and 'items' in p and 'type' in p.get('items') and p.get('items').type in POD_TYPES:
106+
pc = dup_property()
107+
fd[pn] = SchemaEntry(CTYPE_ARRAY, pc.get('items'), pc)
108+
elif p.type == 'object':
109+
if util.is_map_prop(p):
110+
if 'type' in p.additionalProperties and p.additionalProperties.type in POD_TYPES:
111+
pc = dup_property()
112+
fd[pn] = SchemaEntry(CTYPE_MAP, pc.additionalProperties, pc)
113+
else:
114+
set_nested_schema(to_cli_schema(c, c.schemas[util.nested_type_name(schema.id, pn)]))
115+
elif p.type in POD_TYPES:
116+
pc = dup_property()
117+
fd[pn] = SchemaEntry(CTYPE_POD, pc, pc)
118+
# end handle property type
119+
# end
120+
121+
return res
122+
123+
124+
# Convert the given cli-schema (result from to_cli_schema(schema)) to a yaml-like string. It's suitable for
125+
# documentation only
126+
def cli_schema_to_yaml(schema, prefix=''):
127+
if not prefix:
128+
o = '%s%s:\n' % (prefix, util.unique_type_name(schema.id))
129+
else:
130+
o = ''
131+
prefix += ' '
132+
for fn, f in schema.fields.iteritems():
133+
o += '%s%s:' % (prefix, mangle_subcommand(fn))
134+
if not isinstance(f, SchemaEntry):
135+
o += '\n' + cli_schema_to_yaml(f, prefix)
136+
else:
137+
t = f.actual_property.type
138+
if f.container_type == CTYPE_ARRAY:
139+
t = '[%s]' % t
140+
elif f.container_type == CTYPE_MAP:
141+
t = '{ string: %s }' % t
142+
o += ' %s\n' % t
143+
# end for each field
144+
return o
145+
146+
64147
# split the result along split segments
65148
def process_template_result(r, output_file):
66149
found = False
@@ -74,7 +157,7 @@ def process_template_result(r, output_file):
74157
for m in re_splitters.finditer(r):
75158
found = True
76159
fh = open(os.path.join(dir, m.group(1)), 'wb')
77-
fh.write(m.group(2))
160+
fh.write(m.group(2).encode('UTF-8'))
78161
fh.close()
79162
# end for each match
80163

src/mako/lib/util.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ def mangle_ident(n):
297297
return n + '_'
298298
return n
299299

300-
def _is_map_prop(p):
300+
def is_map_prop(p):
301301
return 'additionalProperties' in p
302302

303303
def _assure_unique_type_name(schemas, tn):
@@ -342,7 +342,7 @@ def wrap_type(tn):
342342
if t.type == 'array':
343343
return wrap_type("%s<%s>" % (rust_type, unique_type_name((nested_type(t)))))
344344
elif t.type == 'object':
345-
if _is_map_prop(t):
345+
if is_map_prop(t):
346346
return wrap_type("%s<String, %s>" % (rust_type, nested_type(t)))
347347
else:
348348
return wrap_type(nested_type(t))
@@ -725,7 +725,7 @@ def recurse_properties(prefix, rs, s, parent_ids):
725725
ns.update((k, deepcopy(v)) for k, v in p.items.iteritems())
726726

727727
recurse_properties(ns.id, ns, ns, append_unique(parent_ids, rs.id))
728-
elif _is_map_prop(p):
728+
elif is_map_prop(p):
729729
recurse_properties(nested_type_name(prefix, pn), rs,
730730
p.additionalProperties, append_unique(parent_ids, rs.id))
731731
elif 'items' in p:
@@ -838,7 +838,10 @@ def supports_scopes(auth):
838838
return bool(auth) and bool(auth.oauth2)
839839

840840
# Returns th desired scope for the given method. It will use read-only scopes for read-only methods
841+
# May be None no scope-based authentication is required
841842
def method_default_scope(m):
843+
if 'scopes' not in m:
844+
return None
842845
default_scope = sorted(m.scopes)[0]
843846
if m.httpMethod in ('HEAD', 'GET', 'OPTIONS', 'TRACE'):
844847
for scope in m.scopes:

0 commit comments

Comments
 (0)