Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / tools / telemetry / third_party / webpagereplay / rules_parser.py
blob109db6de67cabb28fa1d3d63b054b93ee226142f
1 # Copyright 2015 Google Inc. All Rights Reserved.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
15 r"""Rules parser.
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}"}}
27 """
29 import json
30 import re
33 class Error(Exception):
34 pass
37 class Rules(object):
39 """A parsed sequence of Rule objects."""
41 def __init__(self, file_obj=None, allowed_imports=None):
42 """Initializes from the given file object.
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)
53 def Contains(self, rule_type_name):
54 """Returns true if any rule matches the given type name.
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))
63 def Find(self, rule_type_name):
64 """Returns a _Rule object containing all rules with the given type name.
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)
79 def __str__(self):
80 return _ToString(self._rules)
82 def __repr__(self):
83 return str(self)
86 class _Rule(object):
87 """Calls a sequence of Rule objects until one returns should_stop."""
89 def __init__(self, rules):
90 self._rules = rules
92 def __call__(self, request, response):
93 """Calls the rules until one returns should_stop.
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.
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
110 def __str__(self):
111 return _ToString(self._rules)
113 def __repr__(self):
114 return str(self)
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)
122 def _Load(file_obj, allowed_imports):
123 """Parses and evaluates all rules in the given file.
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.
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
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)
157 module = __import__(modulename, fromlist=[classname])
158 clazz = getattr(module, classname)
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))
164 rule = clazz(**args)
166 rules.append(rule)
167 return rules