OLD | NEW |
(Empty) | |
| 1 # Copyright 2016 Google Inc. All Rights Reserved. |
| 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 """Implements a utility for parsing and formatting path templates.""" |
| 16 |
| 17 from __future__ import absolute_import |
| 18 from collections import namedtuple |
| 19 |
| 20 from ply import lex, yacc |
| 21 |
| 22 _BINDING = 1 |
| 23 _END_BINDING = 2 |
| 24 _TERMINAL = 3 |
| 25 _Segment = namedtuple('_Segment', ['kind', 'literal']) |
| 26 |
| 27 |
| 28 def _format(segments): |
| 29 template = '' |
| 30 slash = True |
| 31 for segment in segments: |
| 32 if segment.kind == _TERMINAL: |
| 33 if slash: |
| 34 template += '/' |
| 35 template += segment.literal |
| 36 slash = True |
| 37 if segment.kind == _BINDING: |
| 38 template += '/{%s=' % segment.literal |
| 39 slash = False |
| 40 if segment.kind == _END_BINDING: |
| 41 template += '%s}' % segment.literal |
| 42 return template[1:] # Remove the leading / |
| 43 |
| 44 |
| 45 class ValidationException(Exception): |
| 46 """Represents a path template validation error.""" |
| 47 pass |
| 48 |
| 49 |
| 50 class PathTemplate(object): |
| 51 """Represents a path template.""" |
| 52 |
| 53 segments = None |
| 54 segment_count = 0 |
| 55 |
| 56 def __init__(self, data): |
| 57 parser = _Parser() |
| 58 self.segments = parser.parse(data) |
| 59 self.verb = parser.verb |
| 60 self.segment_count = parser.segment_count |
| 61 |
| 62 def __len__(self): |
| 63 return self.segment_count |
| 64 |
| 65 def __repr__(self): |
| 66 return _format(self.segments) |
| 67 |
| 68 def render(self, bindings): |
| 69 """Renders a string from a path template using the provided bindings. |
| 70 |
| 71 Args: |
| 72 bindings (dict): A dictionary of var names to binding strings. |
| 73 |
| 74 Returns: |
| 75 str: The rendered instantiation of this path template. |
| 76 |
| 77 Raises: |
| 78 ValidationError: If a key isn't provided or if a sub-template can't |
| 79 be parsed. |
| 80 """ |
| 81 out = [] |
| 82 binding = False |
| 83 for segment in self.segments: |
| 84 if segment.kind == _BINDING: |
| 85 if segment.literal not in bindings: |
| 86 raise ValidationException( |
| 87 ('rendering error: value for key \'{}\' ' |
| 88 'not provided').format(segment.literal)) |
| 89 out.extend(PathTemplate(bindings[segment.literal]).segments) |
| 90 binding = True |
| 91 elif segment.kind == _END_BINDING: |
| 92 binding = False |
| 93 else: |
| 94 if binding: |
| 95 continue |
| 96 out.append(segment) |
| 97 path = _format(out) |
| 98 self.match(path) |
| 99 return path |
| 100 |
| 101 def match(self, path): |
| 102 """Matches a fully qualified path template string. |
| 103 |
| 104 Args: |
| 105 path (str): A fully qualified path template string. |
| 106 |
| 107 Returns: |
| 108 dict: Var names to matched binding values. |
| 109 |
| 110 Raises: |
| 111 ValidationException: If path can't be matched to the template. |
| 112 """ |
| 113 this = self.segments |
| 114 that = path.split('/') |
| 115 current_var = None |
| 116 bindings = {} |
| 117 segment_count = self.segment_count |
| 118 j = 0 |
| 119 for i in range(0, len(this)): |
| 120 if j >= len(that): |
| 121 break |
| 122 if this[i].kind == _TERMINAL: |
| 123 if this[i].literal == '*': |
| 124 bindings[current_var] = that[j] |
| 125 j += 1 |
| 126 elif this[i].literal == '**': |
| 127 until = j + len(that) - segment_count + 1 |
| 128 segment_count += len(that) - segment_count |
| 129 bindings[current_var] = '/'.join(that[j:until]) |
| 130 j = until |
| 131 elif this[i].literal != that[j]: |
| 132 raise ValidationException( |
| 133 'mismatched literal: \'%s\' != \'%s\'' % ( |
| 134 this[i].literal, that[j])) |
| 135 else: |
| 136 j += 1 |
| 137 elif this[i].kind == _BINDING: |
| 138 current_var = this[i].literal |
| 139 if j != len(that) or j != segment_count: |
| 140 raise ValidationException( |
| 141 'match error: could not render from the path template: {}' |
| 142 .format(path)) |
| 143 return bindings |
| 144 |
| 145 |
| 146 # pylint: disable=C0103 |
| 147 # pylint: disable=R0201 |
| 148 class _Parser(object): |
| 149 tokens = ( |
| 150 'FORWARD_SLASH', |
| 151 'LEFT_BRACE', |
| 152 'RIGHT_BRACE', |
| 153 'EQUALS', |
| 154 'WILDCARD', |
| 155 'PATH_WILDCARD', |
| 156 'LITERAL', |
| 157 ) |
| 158 |
| 159 t_FORWARD_SLASH = r'/' |
| 160 t_LEFT_BRACE = r'\{' |
| 161 t_RIGHT_BRACE = r'\}' |
| 162 t_EQUALS = r'=' |
| 163 t_WILDCARD = r'\*' |
| 164 t_PATH_WILDCARD = r'\*\*' |
| 165 t_LITERAL = r'[^*=}{\/]+' |
| 166 |
| 167 t_ignore = ' \t' |
| 168 |
| 169 def __init__(self): |
| 170 self.lexer = lex.lex(module=self) |
| 171 self.parser = yacc.yacc(module=self, debug=False, write_tables=False) |
| 172 self.verb = '' |
| 173 self.binding_var_count = 0 |
| 174 self.segment_count = 0 |
| 175 |
| 176 def parse(self, data): |
| 177 """Returns a list of path template segments parsed from data. |
| 178 |
| 179 Args: |
| 180 data: A path template string. |
| 181 Returns: |
| 182 A list of _Segment. |
| 183 """ |
| 184 self.binding_var_count = 0 |
| 185 self.segment_count = 0 |
| 186 |
| 187 segments = self.parser.parse(data) |
| 188 # Validation step: checks that there are no nested bindings. |
| 189 path_wildcard = False |
| 190 for segment in segments: |
| 191 if segment.kind == _TERMINAL and segment.literal == '**': |
| 192 if path_wildcard: |
| 193 raise ValidationException( |
| 194 'validation error: path template cannot contain more ' |
| 195 'than one path wildcard') |
| 196 path_wildcard = True |
| 197 if segments and segments[-1].kind == _TERMINAL: |
| 198 final_term = segments[-1].literal |
| 199 last_colon_pos = final_term.rfind(':') |
| 200 if last_colon_pos != -1: |
| 201 self.verb = final_term[last_colon_pos + 1:] |
| 202 segments[-1] = _Segment(_TERMINAL, final_term[:last_colon_pos]) |
| 203 |
| 204 return segments |
| 205 |
| 206 def p_template(self, p): |
| 207 """template : FORWARD_SLASH bound_segments |
| 208 | bound_segments""" |
| 209 # ply fails on a negative index. |
| 210 p[0] = p[len(p) - 1] |
| 211 |
| 212 def p_bound_segments(self, p): |
| 213 """bound_segments : bound_segment FORWARD_SLASH bound_segments |
| 214 | bound_segment""" |
| 215 p[0] = p[1] |
| 216 if len(p) > 2: |
| 217 p[0].extend(p[3]) |
| 218 |
| 219 def p_unbound_segments(self, p): |
| 220 """unbound_segments : unbound_terminal FORWARD_SLASH unbound_segments |
| 221 | unbound_terminal""" |
| 222 p[0] = p[1] |
| 223 if len(p) > 2: |
| 224 p[0].extend(p[3]) |
| 225 |
| 226 def p_bound_segment(self, p): |
| 227 """bound_segment : bound_terminal |
| 228 | variable""" |
| 229 p[0] = p[1] |
| 230 |
| 231 def p_unbound_terminal(self, p): |
| 232 """unbound_terminal : WILDCARD |
| 233 | PATH_WILDCARD |
| 234 | LITERAL""" |
| 235 p[0] = [_Segment(_TERMINAL, p[1])] |
| 236 self.segment_count += 1 |
| 237 |
| 238 def p_bound_terminal(self, p): |
| 239 """bound_terminal : unbound_terminal""" |
| 240 if p[1][0].literal in ['*', '**']: |
| 241 p[0] = [_Segment(_BINDING, '$%d' % self.binding_var_count), |
| 242 p[1][0], |
| 243 _Segment(_END_BINDING, '')] |
| 244 self.binding_var_count += 1 |
| 245 else: |
| 246 p[0] = p[1] |
| 247 |
| 248 def p_variable(self, p): |
| 249 """variable : LEFT_BRACE LITERAL EQUALS unbound_segments RIGHT_BRACE |
| 250 | LEFT_BRACE LITERAL RIGHT_BRACE""" |
| 251 p[0] = [_Segment(_BINDING, p[2])] |
| 252 if len(p) > 4: |
| 253 p[0].extend(p[4]) |
| 254 else: |
| 255 p[0].append(_Segment(_TERMINAL, '*')) |
| 256 self.segment_count += 1 |
| 257 p[0].append(_Segment(_END_BINDING, '')) |
| 258 |
| 259 def p_error(self, p): |
| 260 """Raises a parser error.""" |
| 261 if p: |
| 262 raise ValidationException( |
| 263 'parser error: unexpected token \'%s\'' % p.type) |
| 264 else: |
| 265 raise ValidationException('parser error: unexpected EOF') |
| 266 |
| 267 def t_error(self, t): |
| 268 """Raises a lexer error.""" |
| 269 raise ValidationException( |
| 270 'lexer error: illegal character \'%s\'' % t.value[0]) |
OLD | NEW |