1# Copyright 2017 Google LLC 
    2# 
    3# Licensed under the Apache License, Version 2.0 (the "License"); 
    4# you may not use this file except in compliance with the License. 
    5# You may obtain a copy of the License at 
    6# 
    7#     http://www.apache.org/licenses/LICENSE-2.0 
    8# 
    9# Unless required by applicable law or agreed to in writing, software 
    10# distributed under the License is distributed on an "AS IS" BASIS, 
    11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
    12# See the License for the specific language governing permissions and 
    13# limitations under the License. 
    14 
    15"""Expand and validate URL path templates. 
    16 
    17This module provides the :func:`expand` and :func:`validate` functions for 
    18interacting with Google-style URL `path templates`_ which are commonly used 
    19in Google APIs for `resource names`_. 
    20 
    21.. _path templates: https://github.com/googleapis/googleapis/blob 
    22    /57e2d376ac7ef48681554204a3ba78a414f2c533/google/api/http.proto#L212 
    23.. _resource names: https://cloud.google.com/apis/design/resource_names 
    24""" 
    25 
    26from __future__ import unicode_literals 
    27 
    28from collections import deque 
    29import copy 
    30import functools 
    31import re 
    32 
    33# Regular expression for extracting variable parts from a path template. 
    34# The variables can be expressed as: 
    35# 
    36# - "*": a single-segment positional variable, for example: "books/*" 
    37# - "**": a multi-segment positional variable, for example: "shelf/**/book/*" 
    38# - "{name}": a single-segment wildcard named variable, for example 
    39#   "books/{name}" 
    40# - "{name=*}: same as above. 
    41# - "{name=**}": a multi-segment wildcard named variable, for example 
    42#   "shelf/{name=**}" 
    43# - "{name=/path/*/**}": a multi-segment named variable with a sub-template. 
    44_VARIABLE_RE = re.compile( 
    45    r""" 
    46    (  # Capture the entire variable expression 
    47        (?P<positional>\*\*?)  # Match & capture * and ** positional variables. 
    48        | 
    49        # Match & capture named variables {name} 
    50        { 
    51            (?P<name>[^/]+?) 
    52            # Optionally match and capture the named variable's template. 
    53            (?:=(?P<template>.+?))? 
    54        } 
    55    ) 
    56    """, 
    57    re.VERBOSE, 
    58) 
    59 
    60# Segment expressions used for validating paths against a template. 
    61_SINGLE_SEGMENT_PATTERN = r"([^/]+)" 
    62_MULTI_SEGMENT_PATTERN = r"(.+)" 
    63 
    64 
    65def _expand_variable_match(positional_vars, named_vars, match): 
    66    """Expand a matched variable with its value. 
    67 
    68    Args: 
    69        positional_vars (list): A list of positional variables. This list will 
    70            be modified. 
    71        named_vars (dict): A dictionary of named variables. 
    72        match (re.Match): A regular expression match. 
    73 
    74    Returns: 
    75        str: The expanded variable to replace the match. 
    76 
    77    Raises: 
    78        ValueError: If a positional or named variable is required by the 
    79            template but not specified or if an unexpected template expression 
    80            is encountered. 
    81    """ 
    82    positional = match.group("positional") 
    83    name = match.group("name") 
    84    if name is not None: 
    85        try: 
    86            return str(named_vars[name]) 
    87        except KeyError: 
    88            raise ValueError( 
    89                "Named variable '{}' not specified and needed by template " 
    90                "`{}` at position {}".format(name, match.string, match.start()) 
    91            ) 
    92    elif positional is not None: 
    93        try: 
    94            return str(positional_vars.pop(0)) 
    95        except IndexError: 
    96            raise ValueError( 
    97                "Positional variable not specified and needed by template " 
    98                "`{}` at position {}".format(match.string, match.start()) 
    99            ) 
    100    else: 
    101        raise ValueError("Unknown template expression {}".format(match.group(0))) 
    102 
    103 
    104def expand(tmpl, *args, **kwargs): 
    105    """Expand a path template with the given variables. 
    106 
    107    .. code-block:: python 
    108 
    109        >>> expand('users/*/messages/*', 'me', '123') 
    110        users/me/messages/123 
    111        >>> expand('/v1/{name=shelves/*/books/*}', name='shelves/1/books/3') 
    112        /v1/shelves/1/books/3 
    113 
    114    Args: 
    115        tmpl (str): The path template. 
    116        args: The positional variables for the path. 
    117        kwargs: The named variables for the path. 
    118 
    119    Returns: 
    120        str: The expanded path 
    121 
    122    Raises: 
    123        ValueError: If a positional or named variable is required by the 
    124            template but not specified or if an unexpected template expression 
    125            is encountered. 
    126    """ 
    127    replacer = functools.partial(_expand_variable_match, list(args), kwargs) 
    128    return _VARIABLE_RE.sub(replacer, tmpl) 
    129 
    130 
    131def _replace_variable_with_pattern(match): 
    132    """Replace a variable match with a pattern that can be used to validate it. 
    133 
    134    Args: 
    135        match (re.Match): A regular expression match 
    136 
    137    Returns: 
    138        str: A regular expression pattern that can be used to validate the 
    139            variable in an expanded path. 
    140 
    141    Raises: 
    142        ValueError: If an unexpected template expression is encountered. 
    143    """ 
    144    positional = match.group("positional") 
    145    name = match.group("name") 
    146    template = match.group("template") 
    147    if name is not None: 
    148        if not template: 
    149            return _SINGLE_SEGMENT_PATTERN.format(name) 
    150        elif template == "**": 
    151            return _MULTI_SEGMENT_PATTERN.format(name) 
    152        else: 
    153            return _generate_pattern_for_template(template) 
    154    elif positional == "*": 
    155        return _SINGLE_SEGMENT_PATTERN 
    156    elif positional == "**": 
    157        return _MULTI_SEGMENT_PATTERN 
    158    else: 
    159        raise ValueError("Unknown template expression {}".format(match.group(0))) 
    160 
    161 
    162def _generate_pattern_for_template(tmpl): 
    163    """Generate a pattern that can validate a path template. 
    164 
    165    Args: 
    166        tmpl (str): The path template 
    167 
    168    Returns: 
    169        str: A regular expression pattern that can be used to validate an 
    170            expanded path template. 
    171    """ 
    172    return _VARIABLE_RE.sub(_replace_variable_with_pattern, tmpl) 
    173 
    174 
    175def get_field(request, field): 
    176    """Get the value of a field from a given dictionary. 
    177 
    178    Args: 
    179        request (dict | Message): A dictionary or a Message object. 
    180        field (str): The key to the request in dot notation. 
    181 
    182    Returns: 
    183        The value of the field. 
    184    """ 
    185    parts = field.split(".") 
    186    value = request 
    187 
    188    for part in parts: 
    189        if not isinstance(value, dict): 
    190            value = getattr(value, part, None) 
    191        else: 
    192            value = value.get(part) 
    193    if isinstance(value, dict): 
    194        return 
    195    return value 
    196 
    197 
    198def delete_field(request, field): 
    199    """Delete the value of a field from a given dictionary. 
    200 
    201    Args: 
    202        request (dict | Message): A dictionary object or a Message. 
    203        field (str): The key to the request in dot notation. 
    204    """ 
    205    parts = deque(field.split(".")) 
    206    while len(parts) > 1: 
    207        part = parts.popleft() 
    208        if not isinstance(request, dict): 
    209            if hasattr(request, part): 
    210                request = getattr(request, part, None) 
    211            else: 
    212                return 
    213        else: 
    214            request = request.get(part) 
    215    part = parts.popleft() 
    216    if not isinstance(request, dict): 
    217        if hasattr(request, part): 
    218            request.ClearField(part) 
    219        else: 
    220            return 
    221    else: 
    222        request.pop(part, None) 
    223 
    224 
    225def validate(tmpl, path): 
    226    """Validate a path against the path template. 
    227 
    228    .. code-block:: python 
    229 
    230        >>> validate('users/*/messages/*', 'users/me/messages/123') 
    231        True 
    232        >>> validate('users/*/messages/*', 'users/me/drafts/123') 
    233        False 
    234        >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/books/3) 
    235        True 
    236        >>> validate('/v1/{name=shelves/*/books/*}', /v1/shelves/1/tapes/3) 
    237        False 
    238 
    239    Args: 
    240        tmpl (str): The path template. 
    241        path (str): The expanded path. 
    242 
    243    Returns: 
    244        bool: True if the path matches. 
    245    """ 
    246    pattern = _generate_pattern_for_template(tmpl) + "$" 
    247    return True if re.match(pattern, path) is not None else False 
    248 
    249 
    250def transcode(http_options, message=None, **request_kwargs): 
    251    """Transcodes a grpc request pattern into a proper HTTP request following the rules outlined here, 
    252    https://github.com/googleapis/googleapis/blob/master/google/api/http.proto#L44-L312 
    253 
    254     Args: 
    255         http_options (list(dict)): A list of dicts which consist of these keys, 
    256             'method'    (str): The http method 
    257             'uri'       (str): The path template 
    258             'body'      (str): The body field name (optional) 
    259             (This is a simplified representation of the proto option `google.api.http`) 
    260 
    261         message (Message) : A request object (optional) 
    262         request_kwargs (dict) : A dict representing the request object 
    263 
    264     Returns: 
    265         dict: The transcoded request with these keys, 
    266             'method'        (str)   : The http method 
    267             'uri'           (str)   : The expanded uri 
    268             'body'          (dict | Message)  : A dict or a Message representing the body (optional) 
    269             'query_params'  (dict | Message)  : A dict or Message mapping query parameter variables and values 
    270 
    271     Raises: 
    272         ValueError: If the request does not match the given template. 
    273    """ 
    274    transcoded_value = message or request_kwargs 
    275    bindings = [] 
    276    for http_option in http_options: 
    277        request = {} 
    278 
    279        # Assign path 
    280        uri_template = http_option["uri"] 
    281        fields = [ 
    282            (m.group("name"), m.group("template")) 
    283            for m in _VARIABLE_RE.finditer(uri_template) 
    284        ] 
    285        bindings.append((uri_template, fields)) 
    286 
    287        path_args = {field: get_field(transcoded_value, field) for field, _ in fields} 
    288        request["uri"] = expand(uri_template, **path_args) 
    289 
    290        if not validate(uri_template, request["uri"]) or not all(path_args.values()): 
    291            continue 
    292 
    293        # Remove fields used in uri path from request 
    294        leftovers = copy.deepcopy(transcoded_value) 
    295        for path_field, _ in fields: 
    296            delete_field(leftovers, path_field) 
    297 
    298        # Assign body and query params 
    299        body = http_option.get("body") 
    300 
    301        if body: 
    302            if body == "*": 
    303                request["body"] = leftovers 
    304                if message: 
    305                    request["query_params"] = message.__class__() 
    306                else: 
    307                    request["query_params"] = {} 
    308            else: 
    309                try: 
    310                    if message: 
    311                        request["body"] = getattr(leftovers, body) 
    312                        delete_field(leftovers, body) 
    313                    else: 
    314                        request["body"] = leftovers.pop(body) 
    315                except (KeyError, AttributeError): 
    316                    continue 
    317                request["query_params"] = leftovers 
    318        else: 
    319            request["query_params"] = leftovers 
    320        request["method"] = http_option["method"] 
    321        return request 
    322 
    323    bindings_description = [ 
    324        '\n\tURI: "{}"' 
    325        "\n\tRequired request fields:\n\t\t{}".format( 
    326            uri, 
    327            "\n\t\t".join( 
    328                [ 
    329                    'field: "{}", pattern: "{}"'.format(n, p if p else "*") 
    330                    for n, p in fields 
    331                ] 
    332            ), 
    333        ) 
    334        for uri, fields in bindings 
    335    ] 
    336 
    337    raise ValueError( 
    338        "Invalid request." 
    339        "\nSome of the fields of the request message are either not initialized or " 
    340        "initialized with an invalid value." 
    341        "\nPlease make sure your request matches at least one accepted HTTP binding." 
    342        "\nTo match a binding the request message must have all the required fields " 
    343        "initialized with values matching their patterns as listed below:{}".format( 
    344            "\n".join(bindings_description) 
    345        ) 
    346    )