server/api + docs/api: organize responses

This commit is contained in:
rr- 2016-04-28 18:20:50 +02:00
parent 2b69e9b461
commit 0b20132a2f
16 changed files with 228 additions and 268 deletions

440
API.md
View file

@ -62,11 +62,18 @@
3. [Resources](#resources)
- [User](#user)
- [Detailed user](#detailed-user)
- [Tag category](#tag-category)
- [Detailed tag category](#detailed-tag-category)
- [Tag](#tag)
- [Detailed tag](#detailed-tag)
- [Post](#post)
- [Detailed post](#detailed-post)
- [Comment](#comment)
- [Detailed comment](#detailed-comment)
- [Snapshot](#snapshot)
- [Unpaged search result](#unpaged-search-result)
- [Paged search result](#paged-search-result)
4. [Search](#search)
@ -126,16 +133,8 @@ data.
- **Output**
```json5
{
"tagCategories": [
<tag-category>,
<tag-category>,
<tag-category>
]
}
```
...where `<tag-category>` is a [tag category resource](#tag-category).
An [unpaged search result](#unpaged-search-result), for which `<resource>`
is a [tag category resource](#tag-category).
- **Errors**
@ -168,18 +167,7 @@ data.
- **Output**
```json5
{
"tagCategory": <tag-category>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag-category>` is a [tag category resource](#tag-category) and
`snapshots` contain its earlier versions.
A [detailed tag category resource](#detailed-tag-category).
- **Errors**
@ -210,18 +198,7 @@ data.
- **Output**
```json5
{
"tagCategory": <tag-category>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag-category>` is a [tag category resource](#tag-category) and
`snapshots` contain its earlier versions.
A [detailed tag category resource](#detailed-tag-category).
- **Errors**
@ -245,18 +222,7 @@ data.
- **Output**
```json5
{
"tagCategory": <tag-category>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag-category>` is a [tag category resource](#tag-category) and
`snapshots` contain its earlier versions.
A [detailed tag category resource](#detailed-tag-category).
- **Errors**
@ -299,21 +265,8 @@ data.
- **Output**
```json5
{
"query": <query>, // same as in input
"page": <page>, // same as in input
"pageSize": <page-size>,
"total": <total-count>,
"tags": [
<tag>,
<tag>,
<tag>
]
}
```
...where `<tag>` is a [tag resource](#tag) and `query` contains standard
[search query](#search).
A [paged search result resource](#paged-search-result), for which
`<resource>` is a [tag resource](#tag).
- **Errors**
@ -393,18 +346,7 @@ data.
- **Output**
```json5
{
"tag": <tag>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag>` is a [tag resource](#tag) and `snapshots` contain its
earlier versions.
A [detailed tag resource](#detailed-tag).
- **Errors**
@ -447,18 +389,7 @@ data.
- **Output**
```json5
{
"tag": <tag>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag>` is a [tag resource](#tag) and `snapshots` contain its
earlier versions.
A [detailed tag resource](#detailed-tag).
- **Errors**
@ -489,18 +420,7 @@ data.
- **Output**
```json5
{
"tag": <tag>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag>` is a [tag resource](#tag) and `snapshots` contain its
earlier versions.
A [detailed tag resource](#detailed-tag).
- **Errors**
@ -550,18 +470,7 @@ data.
- **Output**
```json5
{
"tag": <tag>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<tag>` is the target [tag resource](#tag) and `snapshots`
contain its earlier versions.
A [detailed tag resource](#detailed-tag) containing the merged tag.
- **Errors**
@ -619,23 +528,7 @@ data.
- **Output**
```json5
{
"post": <post>,
"snapshots": {
<snapshot>,
<snapshot>,
<snapshot>
},
"comments": {
<comment>,
<comment>,
<comment>
}
}
```
...where `<post>` is a [post resource](#post), `<comment>` is a [comment
resource](#comment) and `snapshots` contain post's earlier versions.
A [detailed post resource](#detailed-post).
- **Errors**
@ -683,12 +576,7 @@ data.
- **Output**
```json5
{
"post": <post>
}
```
...where `<post>` is a [post resource](#post).
A [detailed post resource](#detailed-post).
- **Errors**
@ -709,18 +597,7 @@ data.
- **Output**
```json5
{
"post": <post>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<post>` is a [post resource](#post) and `snapshots` contain its
earlier versions.
A [detailed post resource](#detailed-post).
- **Errors**
@ -740,18 +617,7 @@ data.
- **Output**
```json5
{
"post": <post>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<post>` is a [post resource](#post) and `snapshots` contain its
earlier versions.
A [detailed post resource](#detailed-post).
- **Errors**
@ -770,21 +636,8 @@ data.
- **Output**
```json5
{
"query": <query>, // same as in input
"page": <page>, // same as in input
"pageSize": <page-size>,
"total": <total-count>,
"comments": [
<comment>,
<comment>,
<comment>
]
}
```
...where `<comment>` is a [comment resource](#comment) and `query` contains
standard [search query](#search).
A [paged search result resource](#paged-search-result), for which
`<resource>` is a [comment resource](#comment).
- **Errors**
@ -848,12 +701,7 @@ data.
- **Output**
```json5
{
"comment": <comment>
}
```
...where `<comment>` is a [comment resource](#comment).
A [detailed comment resource](#detailed-comment).
- **Errors**
@ -881,12 +729,7 @@ data.
- **Output**
```json5
{
"comment": <comment>
}
```
...where `<comment>` is a [comment resource](#comment).
A [detailed comment resource](#detailed-comment).
- **Errors**
@ -906,12 +749,7 @@ data.
- **Output**
```json5
{
"comment": <comment>
}
```
...where `<comment>` is a [comment resource](#comment).
A [detailed comment resource](#detailed-comment).
- **Errors**
@ -959,12 +797,7 @@ data.
- **Output**
```json5
{
"comment": <comment>
}
```
...where `<comment>` is a [comment resource](#comment).
A [detailed comment resource](#detailed-comment).
- **Errors**
@ -985,21 +818,8 @@ data.
- **Output**
```json5
{
"query": <query>, // same as in input
"page": <page>, // same as in input
"pageSize": <page-size>,
"total": <total-count>,
"users": [
<user>,
<user>,
<user>
]
}
```
...where `<user>` is a [user resource](#user) and `query` contains standard
[search query](#search).
A [paged search result resource](#paged-search-result), for which
`<resource>` is a [user resource](#user).
- **Errors**
@ -1066,12 +886,7 @@ data.
- **Output**
```json5
{
"user": <user>
}
```
...where `<user>` is a [user resource](#user).
A [detailed user resource](#detailed-user).
- **Errors**
@ -1118,12 +933,7 @@ data.
- **Output**
```json5
{
"user": <user>
}
```
...where `<user>` is a [user resource](#user).
A [detailed user resource](#detailed-user).
- **Errors**
@ -1153,12 +963,7 @@ data.
- **Output**
```json5
{
"user": <user>
}
```
...where `<user>` is a [user resource](#user).
A [detailed user resource](#detailed-user).
- **Errors**
@ -1256,21 +1061,8 @@ data.
- **Output**
```json5
{
"query": <query>, // same as in input
"page": <page>, // same as in input
"pageSize": <page-size>,
"total": <total-count>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
...where `<snapshot>` is a [snapshot resource](#snapshot) and `query`
contains standard [search query](#search).
A [paged search result resource](#paged-search-result), for which
`<resource>` is a [snapshot resource](#snapshot).
- **Errors**
@ -1367,6 +1159,23 @@ A single user.
- `<avatarUrl>`: the URL to the avatar.
## Detailed user
**Description**
A wrapper for a user. In the future, it might offer extra information.
**Structure**
```json5
{
"user": <user>
}
```
**Field meaning**
- `<user>`: a [user resource](#user).
## Tag category
**Description**
@ -1388,6 +1197,30 @@ experience.
- `<name>`: the category name.
- `<color>`: the category color.
## Detailed tag category
**Description**
A tag category with extra information.
**Structure**
```json5
{
"tagCategory": <tag-category>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
**Field meaning**
- `<tag-category>`: a [tag category resource](#tag-category)
- `<snapshot>`: a [snapshot resource](#snapshot) that contains the tag
category's earlier versions.
## Tag
**Description**
@ -1418,6 +1251,29 @@ A single tag. Tags are used to let users search for posts.
- `<creation-time>`: time the tag was created, formatted as per RFC 3339.
- `<last-edit-time>`: time the tag was edited, formatted as per RFC 3339.
## Detailed tag
**Description**
A tag with extra information.
**Structure**
```json5
{
"tag": <tag>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
]
}
```
**Field meaning**
- `<tag>`: a [tag resource](#tag)
- `<snapshot>`: a [snapshot resource](#snapshot) that contains the tag's
earlier versions.
## Post
**Description**
@ -1444,7 +1300,7 @@ One file together with its metadata posted to the site.
"ownScore": <own-score>,
"favoritedBy": <favorited-by>,
"featureCount": <feature-count>,
"lastFeatureTime": <last-feature-time>,
"lastFeatureTime": <last-feature-time>
}
```
@ -1490,6 +1346,35 @@ One file together with its metadata posted to the site.
- `<last-feature-time>`: the last time the post was featured, formatted as per
RFC 3339.
## Detailed post
**Description**
A post with extra information.
**Structure**
```json5
{
"post": <post>,
"snapshots": [
<snapshot>,
<snapshot>,
<snapshot>
],
"comments": {
<comment>,
<comment>,
<comment>
}
}
```
**Field meaning**
- `<post>`: a [post resource](#post).
- `<snapshot>`: a [snapshot resource](#snapshot) that contains the post's
earlier versions.
- `<comment>`: a [comment resource](#comment) for given post.
## Comment
**Description**
@ -1520,6 +1405,21 @@ A comment under a post.
- `<own-score>`: the score (+1/-1 rating) of the given comment by the
authenticated user.
## Detailed comment
**Description**
A wrapper for a comment. In the future, it might offer extra information.
**Structure**
```json5
{
"comment": <comment>
}
```
**Field meaning**
- `<comment>`: a [comment resource](#comment).
## Snapshot
**Description**
@ -1614,6 +1514,58 @@ A snapshot is a version of a database resource.
- `<time>`: when the snapshot was created (i.e. when the resource was changed),
formatted as per RFC 3339.
## Unpaged search result
**Description**
A result of search operation that doesn't involve paging.
**Structure**
```json5
{
"results": [
<resource>,
<resource>,
<resource>
]
}
```
**Field meaning**
- `<resource>`: any resource - which exactly depends on the API call. For
details on this field, check the documentation for given API call.
## Paged search result
**Description**
A result of search operation that involves paging.
**Structure**
```json5
{
"query": <query>, // same as in input
"page": <page>, // same as in input
"pageSize": <page-size>,
"total": <total-count>,
"results": [
<resource>,
<resource>,
<resource>
]
}
```
**Field meaning**
- `<query>`: the query passed in the original request that contains standard
[search query](#search).
- `<page>`: the page number, passed in the original request.
- `<page-size>`: number of records on one page.
- `<total-count>`: how many resources were found. To get the page count, divide
this number by `<page-size>`.
- `<resource>`: any resource - which exactly depends on the API call. For
details on this field, check the documentation for given API call.
# Search

View file

@ -13,8 +13,7 @@ class CommentListApi(BaseApi):
auth.verify_privilege(ctx.user, 'comments:list')
return self._search_executor.execute_and_serialize(
ctx,
lambda comment: comments.serialize_comment(comment, ctx.user),
'comments')
lambda comment: comments.serialize_comment(comment, ctx.user))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'comments:create')
@ -24,13 +23,13 @@ class CommentListApi(BaseApi):
comment = comments.create_comment(ctx.user, post, text)
ctx.session.add(comment)
ctx.session.commit()
return {'comment': comments.serialize_comment(comment, ctx.user)}
return comments.serialize_comment_with_details(comment, ctx.user)
class CommentDetailApi(BaseApi):
def get(self, ctx, comment_id):
auth.verify_privilege(ctx.user, 'comments:view')
comment = comments.get_comment_by_id(comment_id)
return {'comment': comments.serialize_comment(comment, ctx.user)}
return comments.serialize_comment_with_details(comment, ctx.user)
def put(self, ctx, comment_id):
comment = comments.get_comment_by_id(comment_id)
@ -40,7 +39,7 @@ class CommentDetailApi(BaseApi):
comment.last_edit_time = datetime.datetime.now()
comments.update_comment_text(comment, text)
ctx.session.commit()
return {'comment': comments.serialize_comment(comment, ctx.user)}
return comments.serialize_comment_with_details(comment, ctx.user)
def delete(self, ctx, comment_id):
comment = comments.get_comment_by_id(comment_id)
@ -57,11 +56,11 @@ class CommentScoreApi(BaseApi):
comment = comments.get_comment_by_id(comment_id)
scores.set_score(comment, ctx.user, score)
ctx.session.commit()
return {'comment': comments.serialize_comment(comment, ctx.user)}
return comments.serialize_comment_with_details(comment, ctx.user)
def delete(self, ctx, comment_id):
auth.verify_privilege(ctx.user, 'comments:score')
comment = comments.get_comment_by_id(comment_id)
scores.delete_score(comment, ctx.user)
ctx.session.commit()
return {'comment': comments.serialize_comment(comment, ctx.user)}
return comments.serialize_comment_with_details(comment, ctx.user)

View file

@ -43,11 +43,11 @@ class PostScoreApi(BaseApi):
score = ctx.get_param_as_int('score', required=True)
scores.set_score(post, ctx.user, score)
ctx.session.commit()
return {'post': posts.serialize_post(post, ctx.user)}
return posts.serialize_post_with_details(post, ctx.user)
def delete(self, ctx, post_id):
auth.verify_privilege(ctx.user, 'posts:score')
post = posts.get_post_by_id(post_id)
scores.delete_score(post, ctx.user)
ctx.session.commit()
return {'post': posts.serialize_post(post, ctx.user)}
return posts.serialize_post_with_details(post, ctx.user)

View file

@ -10,4 +10,4 @@ class SnapshotListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'snapshots:list')
return self._search_executor.execute_and_serialize(
ctx, snapshots.serialize_snapshot, 'snapshots')
ctx, snapshots.serialize_snapshot)

View file

@ -11,7 +11,7 @@ class TagListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'tags:list')
return self._search_executor.execute_and_serialize(
ctx, tags.serialize_tag, 'tags')
ctx, tags.serialize_tag)
def post(self, ctx):
auth.verify_privilege(ctx.user, 'tags:create')

View file

@ -6,7 +6,7 @@ class TagCategoryListApi(BaseApi):
auth.verify_privilege(ctx.user, 'tag_categories:list')
categories = tag_categories.get_all_categories()
return {
'tagCategories': [
'results': [
tag_categories.serialize_category(category) \
for category in categories],
}

View file

@ -10,7 +10,7 @@ class UserListApi(BaseApi):
def get(self, ctx):
auth.verify_privilege(ctx.user, 'users:list')
return self._search_executor.execute_and_serialize(
ctx, lambda user: users.serialize_user(user, ctx.user), 'users')
ctx, lambda user: users.serialize_user(user, ctx.user))
def post(self, ctx):
auth.verify_privilege(ctx.user, 'users:create')
@ -27,13 +27,13 @@ class UserListApi(BaseApi):
ctx.get_file('avatar'))
ctx.session.add(user)
ctx.session.commit()
return {'user': users.serialize_user(user, ctx.user)}
return users.serialize_user_with_details(user, ctx.user)
class UserDetailApi(BaseApi):
def get(self, ctx, user_name):
auth.verify_privilege(ctx.user, 'users:view')
user = users.get_user_by_name(user_name)
return {'user': users.serialize_user(user, ctx.user)}
return users.serialize_user_with_details(user, ctx.user)
def put(self, ctx, user_name):
user = users.get_user_by_name(user_name)
@ -57,7 +57,7 @@ class UserDetailApi(BaseApi):
ctx.get_param_as_string('avatarStyle'),
ctx.get_file('avatar'))
ctx.session.commit()
return {'user': users.serialize_user(user, ctx.user)}
return users.serialize_user_with_details(user, ctx.user)
def delete(self, ctx, user_name):
user = users.get_user_by_name(user_name)

View file

@ -18,6 +18,9 @@ def serialize_comment(comment, authenticated_user):
ret['ownScore'] = scores.get_score(comment, authenticated_user)
return ret
def serialize_comment_with_details(comment, authenticated_user):
return {'comment': serialize_comment(comment, authenticated_user)}
def try_get_comment_by_id(comment_id):
return db.session \
.query(db.Comment) \

View file

@ -41,6 +41,9 @@ def serialize_user(user, authenticated_user):
return ret
def serialize_user_with_details(user, authenticated_user):
return {'user': serialize_user(user, authenticated_user)}
def get_user_count():
return db.session.query(db.User).count()

View file

@ -28,7 +28,7 @@ class SearchExecutor(object):
.scalar()
return (count, entities)
def execute_and_serialize(self, ctx, serializer, key_name):
def execute_and_serialize(self, ctx, serializer):
query = ctx.get_param_as_string('query')
page = ctx.get_param_as_int('page', default=1, min=1)
page_size = ctx.get_param_as_int('pageSize', default=100, min=1, max=100)
@ -38,7 +38,7 @@ class SearchExecutor(object):
'page': page,
'pageSize': page_size,
'total': count,
key_name: [serializer(entity) for entity in entities],
'results': [serializer(entity) for entity in entities],
}
def _prepare(self, query_text):

View file

@ -34,7 +34,7 @@ def test_retrieving_multiple(test_ctx):
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 2
assert [c['text'] for c in result['comments']] == ['text 1', 'text 2']
assert [c['text'] for c in result['results']] == ['text 1', 'text 2']
def test_trying_to_retrieve_multiple_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):

View file

@ -38,13 +38,16 @@ def test_featuring(test_ctx):
assert posts.try_get_featured_post().post_id == 1
assert posts.get_post_by_id(1).is_featured
assert 'post' in result
assert 'snapshots' in result
assert 'id' in result['post']
assert 'snapshots' in result
assert 'comments' in result
result = test_ctx.api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')))
assert 'post' in result
assert 'id' in result['post']
assert 'snapshots' in result
assert 'comments' in result
def test_trying_to_feature_the_same_post_twice(test_ctx):
db.session.add(test_ctx.post_factory(id=1))

View file

@ -41,7 +41,7 @@ def test_retrieving_multiple(test_ctx):
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 2
assert len(result['snapshots']) == 2
assert len(result['results']) == 2
def test_trying_to_retrieve_multiple_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):

View file

@ -31,7 +31,7 @@ def test_retrieving_multiple(test_ctx):
result = test_ctx.list_api.get(
test_ctx.context_factory(
user=test_ctx.user_factory(rank='regular_user')))
assert [cat['name'] for cat in result['tagCategories']] == ['c1', 'c2']
assert [cat['name'] for cat in result['results']] == ['c1', 'c2']
def test_retrieving_single(test_ctx):
db.session.add(test_ctx.tag_category_factory(name='cat'))

View file

@ -34,7 +34,7 @@ def test_retrieving_multiple(test_ctx):
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 2
assert [t['names'] for t in result['tags']] == [['t1'], ['t2']]
assert [t['names'] for t in result['results']] == [['t1'], ['t2']]
def test_trying_to_retrieve_multiple_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):

View file

@ -33,7 +33,7 @@ def test_retrieving_multiple(test_ctx):
assert result['page'] == 1
assert result['pageSize'] == 100
assert result['total'] == 2
assert [u['name'] for u in result['users']] == ['u1', 'u2']
assert [u['name'] for u in result['results']] == ['u1', 'u2']
def test_trying_to_retrieve_multiple_without_privileges(test_ctx):
with pytest.raises(errors.AuthError):