rules_parser.py 5.1 KB

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