| OLD | NEW |
| (Empty) |
| 1 # Copyright 2015 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 r"""Rules parser. | |
| 16 | |
| 17 The input syntax is: | |
| 18 [{"comment": ignored_value}, | |
| 19 {"rule_class_name1": {"arg1": value, "arg2": value, ...}}, | |
| 20 {"rule_class_name2": {"arg1": value, "arg2": value, ...}}, | |
| 21 ...] | |
| 22 E.g.: | |
| 23 [{"comment": "this text is ignored"}, | |
| 24 {"SendStatus": {"url": "example\\.com/ss.*", "status": 204}}, | |
| 25 {"ModifyUrl": {"url": "(example\\.com)(/.*)", "new_url": "{1}"}} | |
| 26 ] | |
| 27 """ | |
| 28 | |
| 29 import json | |
| 30 import re | |
| 31 | |
| 32 | |
| 33 class Error(Exception): | |
| 34 pass | |
| 35 | |
| 36 | |
| 37 class Rules(object): | |
| 38 | |
| 39 """A parsed sequence of Rule objects.""" | |
| 40 | |
| 41 def __init__(self, file_obj=None, allowed_imports=None): | |
| 42 """Initializes from the given file object. | |
| 43 | |
| 44 Args: | |
| 45 file_obj: A file object. | |
| 46 allowed_imports: A set of strings, defaults to {'rules'}. | |
| 47 Use {'*'} to allow any import path. | |
| 48 """ | |
| 49 if allowed_imports is None: | |
| 50 allowed_imports = {'rules'} | |
| 51 self._rules = [] if file_obj is None else _Load(file_obj, allowed_imports) | |
| 52 | |
| 53 def Contains(self, rule_type_name): | |
| 54 """Returns true if any rule matches the given type name. | |
| 55 | |
| 56 Args: | |
| 57 rule_type_name: a string. | |
| 58 Returns: | |
| 59 True if any rule matches, else False. | |
| 60 """ | |
| 61 return any(rule for rule in self._rules if rule.IsType(rule_type_name)) | |
| 62 | |
| 63 def Find(self, rule_type_name): | |
| 64 """Returns a _Rule object containing all rules with the given type name. | |
| 65 | |
| 66 Args: | |
| 67 rule_type_name: a string. | |
| 68 Returns: | |
| 69 A callable object that expects two arguments: | |
| 70 request: the httparchive ArchivedHttpRequest | |
| 71 response: the httparchive ArchivedHttpResponse | |
| 72 and returns the rule return_value of the first rule that returns | |
| 73 should_stop == True, or the last rule's return_value if all rules returns | |
| 74 should_stop == False. | |
| 75 """ | |
| 76 matches = [rule for rule in self._rules if rule.IsType(rule_type_name)] | |
| 77 return _Rule(matches) | |
| 78 | |
| 79 def __str__(self): | |
| 80 return _ToString(self._rules) | |
| 81 | |
| 82 def __repr__(self): | |
| 83 return str(self) | |
| 84 | |
| 85 | |
| 86 class _Rule(object): | |
| 87 """Calls a sequence of Rule objects until one returns should_stop.""" | |
| 88 | |
| 89 def __init__(self, rules): | |
| 90 self._rules = rules | |
| 91 | |
| 92 def __call__(self, request, response): | |
| 93 """Calls the rules until one returns should_stop. | |
| 94 | |
| 95 Args: | |
| 96 request: the httparchive ArchivedHttpRequest. | |
| 97 response: the httparchive ArchivedHttpResponse, which may be None. | |
| 98 Returns: | |
| 99 The rule return_value of the first rule that returns should_stop == True, | |
| 100 or the last rule's return_value if all rules return should_stop == False. | |
| 101 """ | |
| 102 return_value = None | |
| 103 for rule in self._rules: | |
| 104 should_stop, return_value = rule.ApplyRule( | |
| 105 return_value, request, response) | |
| 106 if should_stop: | |
| 107 break | |
| 108 return return_value | |
| 109 | |
| 110 def __str__(self): | |
| 111 return _ToString(self._rules) | |
| 112 | |
| 113 def __repr__(self): | |
| 114 return str(self) | |
| 115 | |
| 116 | |
| 117 def _ToString(rules): | |
| 118 """Formats a sequence of Rule objects into a string.""" | |
| 119 return '[\n%s\n]' % '\n'.join('%s' % rule for rule in rules) | |
| 120 | |
| 121 | |
| 122 def _Load(file_obj, allowed_imports): | |
| 123 """Parses and evaluates all rules in the given file. | |
| 124 | |
| 125 Args: | |
| 126 file_obj: a file object. | |
| 127 allowed_imports: a sequence of strings, e.g.: {'rules'}. | |
| 128 Returns: | |
| 129 a list of rules. | |
| 130 """ | |
| 131 rules = [] | |
| 132 entries = json.load(file_obj) | |
| 133 if not isinstance(entries, list): | |
| 134 raise Error('Expecting a list, not %s', type(entries)) | |
| 135 for i, entry in enumerate(entries): | |
| 136 if not isinstance(entry, dict): | |
| 137 raise Error('%s: Expecting a dict, not %s', i, type(entry)) | |
| 138 if len(entry) != 1: | |
| 139 raise Error('%s: Expecting 1 item, not %d', i, len(entry)) | |
| 140 name, args = next(entry.iteritems()) | |
| 141 if not isinstance(name, basestring): | |
| 142 raise Error('%s: Expecting a string TYPE, not %s', i, type(name)) | |
| 143 if not re.match(r'(\w+\.)*\w+$', name): | |
| 144 raise Error('%s: Expecting a classname TYPE, not %s', i, name) | |
| 145 if name == 'comment': | |
| 146 continue | |
| 147 if not isinstance(args, dict): | |
| 148 raise Error('%s: Expecting a dict ARGS, not %s', i, type(args)) | |
| 149 fullname = str(name) | |
| 150 if '.' not in fullname: | |
| 151 fullname = 'rules.%s' % fullname | |
| 152 | |
| 153 modulename, classname = fullname.rsplit('.', 1) | |
| 154 if '*' not in allowed_imports and modulename not in allowed_imports: | |
| 155 raise Error('%s: Package %r is not in allowed_imports', i, modulename) | |
| 156 | |
| 157 module = __import__(modulename, fromlist=[classname]) | |
| 158 clazz = getattr(module, classname) | |
| 159 | |
| 160 missing = {s for s in ('IsType', 'ApplyRule') if not hasattr(clazz, s)} | |
| 161 if missing: | |
| 162 raise Error('%s: %s lacks %s', i, clazz.__name__, ' and '.join(missing)) | |
| 163 | |
| 164 rule = clazz(**args) | |
| 165 | |
| 166 rules.append(rule) | |
| 167 return rules | |
| OLD | NEW |