Skip to content

Commit

Permalink
Add some meta operators
Browse files Browse the repository at this point in the history
  • Loading branch information
apiad committed Jan 24, 2018
1 parent 1a8cc00 commit 2dc0b12
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 33 deletions.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ layout: default

**jsonapi** is heavily inspired by [graphql](https://graphql.org), but aimed at a much simpler use case. The idea is to have a minimal framework for easily building JSON based APIs, that doesn't require any particular frontend technology. The design is inspired in **graphql**'s idea of a single fully customizable endpoint, but instead of defining a specific query language, **jsonapi** is entirely based on JSON both for the query and the response, requires much less boilerplate code, only works in Python, and of course, is much less battle-tested. If you find **graphql** amazing but would like to try a decaffeinated version that you can setup in 10 lines, then give **jsonapi** a shoot.

## Instalation
## Installation

**jsonapi** is a single Python file with no dependencies that you can just clone and distribute with your project's source code:

Expand Down
35 changes: 33 additions & 2 deletions docs/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ The next thing you'll probably want is to pass some arguments to your functions.

```

You can also use the "lazy" way, which is simply to prepend `"$"` to all arguments:
You can also use the "lazy" way, which is simply to prepend `"$"` to all arguments:

```python
>>> api({ "sum": { "$a": 27, "$b": 15 } })
Expand Down Expand Up @@ -56,4 +56,35 @@ If your function receives a complex argument (i.e., a JSON dict), you will autom

```

You can read more about `JsonObj`'s magic tricks [here](/jsonobj.md).
You can read more about `JsonObj`'s magic tricks [here](/jsonobj.md).

## Collection operators

For collections we have a couple interesting operators that return some aggregated information about the collection itself. These operators are used the same way as standard navigation, but their names start with `_`. In turn, these operators are not applied on the collection's content, but instead on the collection itself.

The `_count` operator returns the number of items in the collection:

```python
>>> class CountApi(JsonApi):
... def elements(self):
... return list(range(10))

>>> api = CountApi()
>>> api({ 'elements' })
{'elements': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}

>>> api({ 'elements': { '_count' }})
{'elements': {'_count': 10}}

```

Now the problem is that you lost the items themselves. Well, this can fixed by calling another operator `_items` which returns back the items of the collection:

```python
>>> from pprint import pprint

>>> r = api({ 'elements': { '_count', '_items' }})
>>> pprint(r)
{'elements': {'_count': 10, '_items': [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]}}

```
90 changes: 60 additions & 30 deletions jsonapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ def __init__(self, **kwargs):
setattr(self, str(k), self._parse(w))

def dict(self):
return {key: self._json(value) for key, value in self.__dict__.items() if not hasattr(value, '__call__')}
return {
key: self._json(value) for key, value
in self.__dict__.items()
if not hasattr(value, '__call__')
}

def json(self, **kwargs):
return json.dumps(self.dict(), **kwargs)
Expand Down Expand Up @@ -53,48 +57,74 @@ def _query(self, obj, query):
if isinstance(obj, (int, float, str, bool)):
return obj

if isinstance(obj, (list, tuple)):
return [self._query(i, query) for i in obj]
if hasattr(obj, '__iter__'):
return self._query_iter(obj, query)

if isinstance(obj, dict):
return {str(k): self._query(v, query) for k, v in obj.items()}

if isinstance(obj, JsonObj):
if query is None:
return {key: value for key, value in obj.__dict__.items() if not hasattr(value, '__call__')}
return self._query_obj(obj, query)

payload = {}
raise TypeError("type %s is not supported" % type(x))

if isinstance(query, dict):
items = query.items()
else:
items = [(key, None) for key in query]
def _query_iter(self, obj, query):
meta = self._extract(query, '_')

for key, value in items:
attr = getattr(obj, key)
args = {}
navigation = value
if not meta:
return [self._query(i, query) for i in obj]

if isinstance(navigation, dict):
if "$" in navigation:
args = {str(k): self._parse(v)
for k, v in navigation.pop("$").items()}
else:
for a in list(navigation.keys()):
if a.startswith("$"):
v = navigation.pop(a)
args[a.strip("$")] = self._parse(v)
result = {}

if hasattr(attr, '__call__'):
result = attr(**args)
else:
result = attr
for m in meta:
result["_%s" % m] = getattr(self, '_meta_%s' % m)(obj, query)

payload[key] = self._query(result, navigation)
return result

return payload
def _query_obj(self, obj, query):
if query is None:
return {key: value for key, value in obj.__dict__.items() if not hasattr(value, '__call__')}

raise TypeError("type %s is not supported" % type(x))
payload = {}

if isinstance(query, dict): items = query.items()
else: items = [(key, None) for key in query]

for key, value in items:
attr = getattr(obj, key)
query = value
args = self._extract(query, "$")

if hasattr(attr, '__call__'): result = attr(**args)
else: result = attr

payload[key] = self._query(result, query)

return payload

def _extract(self, query, label):
if query is None:
return {}

if not isinstance(query, dict):
query = { k: None for k in query }

if label in query:
return {str(k): self._parse(v) for k, v in query.pop(label).items()}

args = {}

for a in list(query):
if a.startswith(label):
args[a.strip(label)] = self._parse(query.pop(a))

return args

def _meta_count(self, obj, query):
return len(obj)

def _meta_items(self, obj, query):
return [self._query(i, query) for i in obj]

def __call__(self, query, encode=False, **kw):
if isinstance(query, str):
Expand Down

0 comments on commit 2dc0b12

Please sign in to comment.