Source code for google.gax.path_template

# Copyright 2016, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Implements a utility for parsing and formatting path templates."""

from __future__ import absolute_import

from collections import namedtuple

from ply import lex, yacc

_BINDING = 1
_END_BINDING = 2
_TERMINAL = 3
_Segment = namedtuple('_Segment', ['kind', 'literal'])


def _format(segments):
    template = ''
    slash = True
    for segment in segments:
        if segment.kind == _TERMINAL:
            if slash:
                template += '/'
            template += segment.literal
        slash = True
        if segment.kind == _BINDING:
            template += '/{%s=' % segment.literal
            slash = False
        if segment.kind == _END_BINDING:
            template += '%s}' % segment.literal
    return template[1:]  # Remove the leading /


[docs]class ValidationException(Exception): """Represents a path template validation error.""" pass
[docs]class PathTemplate(object): """Represents a path template.""" segments = None segment_count = 0 def __init__(self, data): parser = _Parser() self.segments = parser.parse(data) self.segment_count = parser.segment_count def __len__(self): return self.segment_count def __repr__(self): return _format(self.segments)
[docs] def render(self, bindings): """Renders a string from a path template using the provided bindings. Args: bindings (dict): A dictionary of var names to binding strings. Returns: str: The rendered instantiation of this path template. Raises: ValidationException: If a key isn't provided or if a sub-template can't be parsed. """ out = [] binding = False for segment in self.segments: if segment.kind == _BINDING: if segment.literal not in bindings: raise ValidationException( ('rendering error: value for key \'{}\' ' 'not provided').format(segment.literal)) out.extend(PathTemplate(bindings[segment.literal]).segments) binding = True elif segment.kind == _END_BINDING: binding = False else: if binding: continue out.append(segment) path = _format(out) self.match(path) return path
[docs] def match(self, path): """Matches a fully qualified path template string. Args: path (str): A fully qualified path template string. Returns: dict: Var names to matched binding values. Raises: ValidationException: If path can't be matched to the template. """ this = self.segments that = path.split('/') current_var = None bindings = {} segment_count = self.segment_count j = 0 for i in range(0, len(this)): if j >= len(that): break if this[i].kind == _TERMINAL: if this[i].literal == '*': bindings[current_var] = that[j] j += 1 elif this[i].literal == '**': until = j + len(that) - segment_count + 1 segment_count += len(that) - segment_count bindings[current_var] = '/'.join(that[j:until]) j = until elif this[i].literal != that[j]: raise ValidationException( 'mismatched literal: \'%s\' != \'%s\'' % ( this[i].literal, that[j])) else: j += 1 elif this[i].kind == _BINDING: current_var = this[i].literal if j != len(that) or j != segment_count: raise ValidationException( 'match error: could not render from the path template: {}' .format(path)) return bindings
# pylint: disable=C0103 # pylint: disable=R0201 class _Parser(object): tokens = ( 'FORWARD_SLASH', 'LEFT_BRACE', 'RIGHT_BRACE', 'EQUALS', 'WILDCARD', 'PATH_WILDCARD', 'LITERAL', ) t_FORWARD_SLASH = r'/' t_LEFT_BRACE = r'\{' t_RIGHT_BRACE = r'\}' t_EQUALS = r'=' t_WILDCARD = r'\*' t_PATH_WILDCARD = r'\*\*' t_LITERAL = r'[^*=}{\/]+' t_ignore = ' \t' binding_var_count = 0 segment_count = 0 def __init__(self): self.lexer = lex.lex(module=self) self.parser = yacc.yacc(module=self, debug=False, write_tables=False) def parse(self, data): """Returns a list of path template segments parsed from data. Args: data (str): A path template string. Returns: Sequence[_PathSegment]: The path template segments. Raises: ValidationException: If the path template is invalid. """ self.binding_var_count = 0 self.segment_count = 0 segments = self.parser.parse(data) # Validation step: checks that there are no nested bindings. path_wildcard = False for segment in segments: if segment.kind == _TERMINAL and segment.literal == '**': if path_wildcard: raise ValidationException( 'validation error: path template cannot contain more ' 'than one path wildcard') path_wildcard = True return segments def p_template(self, p): """template : FORWARD_SLASH bound_segments | bound_segments""" # ply fails on a negative index. p[0] = p[len(p) - 1] def p_bound_segments(self, p): """bound_segments : bound_segment FORWARD_SLASH bound_segments | bound_segment""" p[0] = p[1] if len(p) > 2: p[0].extend(p[3]) def p_unbound_segments(self, p): """unbound_segments : unbound_terminal FORWARD_SLASH unbound_segments | unbound_terminal""" p[0] = p[1] if len(p) > 2: p[0].extend(p[3]) def p_bound_segment(self, p): """bound_segment : bound_terminal | variable""" p[0] = p[1] def p_unbound_terminal(self, p): """unbound_terminal : WILDCARD | PATH_WILDCARD | LITERAL""" p[0] = [_Segment(_TERMINAL, p[1])] self.segment_count += 1 def p_bound_terminal(self, p): """bound_terminal : unbound_terminal""" if p[1][0].literal in ['*', '**']: p[0] = [_Segment(_BINDING, '$%d' % self.binding_var_count), p[1][0], _Segment(_END_BINDING, '')] self.binding_var_count += 1 else: p[0] = p[1] def p_variable(self, p): """variable : LEFT_BRACE LITERAL EQUALS unbound_segments RIGHT_BRACE | LEFT_BRACE LITERAL RIGHT_BRACE""" p[0] = [_Segment(_BINDING, p[2])] if len(p) > 4: p[0].extend(p[4]) else: p[0].append(_Segment(_TERMINAL, '*')) self.segment_count += 1 p[0].append(_Segment(_END_BINDING, '')) def p_error(self, p): """Raises a parser error.""" if p: raise ValidationException( 'parser error: unexpected token \'%s\'' % p.type) else: raise ValidationException('parser error: unexpected EOF') def t_error(self, t): """Raises a lexer error.""" raise ValidationException( 'lexer error: illegal character \'%s\'' % t.value[0])