First version of new RESTful AFE interface.  Includes a substantial library (under frontend/shared) and a definition of the interface for AFE (frontend/afe/resources.py).

If you want to see what this interface looks like, I encourage you to check out

http://your-autotest-server/afe/server/resources/?alt=json-html

>From there you can explore the entire interface through your browser (this is one of its great strengths).

For an introduction to the idea behind RESTful services, try http://bitworking.org/news/How_to_create_a_REST_Protocol.

This is still very much under development and there are plenty of TODOs, but it's working so I wanted to get it out there so we can start seeing how useful it turns out to be.

Signed-off-by: Steve Howard <showard@google.com>



git-svn-id: http://test.kernel.org/svn/autotest/trunk@4165 592f7852-d20e-0410-864c-8624ca9c26a4
diff --git a/frontend/shared/query_lib.py b/frontend/shared/query_lib.py
new file mode 100644
index 0000000..87ebac2
--- /dev/null
+++ b/frontend/shared/query_lib.py
@@ -0,0 +1,161 @@
+from autotest_lib.frontend.shared import exceptions
+
+class ConstraintError(Exception):
+    """Raised when an error occurs applying a Constraint."""
+
+
+class BaseQueryProcessor(object):
+    # maps selector name to (selector, constraint)
+    _selectors = None
+    _alias_counter = 0
+
+
+    @classmethod
+    def _initialize_selectors(cls):
+        if not cls._selectors:
+            cls._selectors = {}
+            cls._add_all_selectors()
+
+
+    @classmethod
+    def _add_all_selectors(cls):
+        """
+        Subclasses should override this to define which selectors they accept.
+        """
+        pass
+
+
+    @classmethod
+    def _add_field_selector(cls, name, field=None, value_transform=None,
+                            doc=None):
+        if not field:
+            field = name
+        cls._add_selector(Selector(name, doc),
+                          _FieldConstraint(field, value_transform))
+
+
+    @classmethod
+    def _add_related_existence_selector(cls, name, model, field, doc=None):
+        cls._add_selector(Selector(name, doc),
+                          _RelatedExistenceConstraint(model, field,
+                                                      cls.make_alias))
+
+
+    @classmethod
+    def _add_selector(cls, selector, constraint):
+        cls._selectors[selector.name] = (selector, constraint)
+
+
+    @classmethod
+    def make_alias(cls):
+        cls._alias_counter += 1
+        return 'alias%s' % cls._alias_counter
+
+
+    @classmethod
+    def selectors(cls):
+        cls._initialize_selectors()
+        return tuple(selector for selector, constraint
+                     in cls._selectors.itervalues())
+
+
+    @classmethod
+    def has_selector(cls, selector_name):
+        cls._initialize_selectors()
+        return selector_name in cls._selectors
+
+
+    def apply_selector(self, queryset, selector_name, value,
+                       comparison_type='equals', is_inverse=False):
+        _, constraint = self._selectors[selector_name]
+        try:
+            return constraint.apply_constraint(queryset, value, comparison_type,
+                                               is_inverse)
+        except ConstraintError, exc:
+            raise exceptions.BadRequest('Selector %s: %s'
+                                        % (selector_name, exc))
+
+
+    # common value conversions
+
+    @classmethod
+    def read_boolean(cls, boolean_input):
+        if boolean_input.lower() == 'true':
+            return True
+        if boolean_input.lower() == 'false':
+            return False
+        raise exceptions.BadRequest('Invalid input for boolean: %r'
+                                    % boolean_input)
+
+
+class Selector(object):
+    def __init__(self, name, doc):
+        self.name = name
+        self.doc = doc
+
+
+class Constraint(object):
+    def apply_constraint(self, queryset, value, comparison_type, is_inverse):
+        raise NotImplementedError
+
+
+class _FieldConstraint(Constraint):
+    def __init__(self, field, value_transform=None):
+        self._field = field
+        self._value_transform = value_transform
+
+
+    _COMPARISON_MAP = {
+            'equals': 'exact',
+            'lt': 'lt',
+            'le': 'lte',
+            'gt': 'gt',
+            'ge': 'gte',
+            'contains': 'contains',
+            'startswith': 'startswith',
+            'endswith': 'endswith',
+            'in': 'in',
+            }
+
+
+    def apply_constraint(self, queryset, value, comparison_type, is_inverse):
+        if self._value_transform:
+            value = self._value_transform(value)
+
+        kwarg_name = str(self._field + '__' +
+                         self._COMPARISON_MAP[comparison_type])
+
+        if is_inverse:
+            return queryset.exclude(**{kwarg_name: value})
+        else:
+            return queryset.filter(**{kwarg_name: value})
+
+
+class _RelatedExistenceConstraint(Constraint):
+    def __init__(self, model, field, make_alias_fn):
+        self._model = model
+        self._field = field
+        self._make_alias_fn = make_alias_fn
+
+
+    def apply_constraint(self, queryset, value, comparison_type, is_inverse):
+        if comparison_type not in (None, 'equals'):
+            raise ConstraintError('Can only use equals or not equals with '
+                                  'this selector')
+        related_query = self._model.objects.filter(**{self._field: value})
+        if not related_query:
+            raise ConstraintError('%s %s not found' % (self._model_name, value))
+        alias = self._make_alias_fn()
+        queryset = queryset.model.objects.join_custom_field(queryset,
+                                                            related_query,
+                                                            alias)
+        if is_inverse:
+            condition = '%s.%s IS NULL'
+        else:
+            condition = '%s.%s IS NOT NULL'
+        condition %= (alias,
+                      queryset.model.objects.key_on_joined_table(related_query))
+
+        queryset = queryset.model.objects.add_where(queryset, condition)
+
+        return queryset