From 0bc1c29dffcaa5a2aa118be5f6ba24db06fa1754 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 14 May 2015 16:47:38 -0400 Subject: [PATCH 1/2] Switch the Python side to Swagger v2 --- endpoints/api/billing.py | 4 +- endpoints/api/build.py | 5 +- endpoints/api/discovery.py | 290 +++++++++++++----------- endpoints/api/image.py | 2 + endpoints/api/logs.py | 2 + endpoints/api/organization.py | 4 +- endpoints/api/permission.py | 2 + endpoints/api/prototype.py | 2 + endpoints/api/repoemail.py | 2 + endpoints/api/repository.py | 2 + endpoints/api/repositorynotification.py | 2 + endpoints/api/repotoken.py | 2 + endpoints/api/robot.py | 6 +- endpoints/api/search.py | 2 + endpoints/api/subscribe.py | 2 + endpoints/api/suconfig.py | 2 + endpoints/api/superuser.py | 2 + endpoints/api/tag.py | 2 + endpoints/api/team.py | 2 + endpoints/api/trigger.py | 4 +- endpoints/api/user.py | 14 +- 21 files changed, 217 insertions(+), 138 deletions(-) diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 36dc96102..32759579f 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -1,3 +1,5 @@ +""" Billing information, subscriptions, and plan information. """ + import stripe from flask import request @@ -352,7 +354,7 @@ class UserInvoiceList(ApiResource): @path_param('orgname', 'The name of the organization') @related_user_resource(UserInvoiceList) @show_if(features.BILLING) -class OrgnaizationInvoiceList(ApiResource): +class OrganizationInvoiceList(ApiResource): """ Resource for listing an orgnaization's invoices. """ @require_scope(scopes.ORG_ADMIN) @nickname('listOrgInvoices') diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 385e9288c..05dd2114b 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -1,3 +1,5 @@ +""" Create, list, cancel and get status/logs of repository builds. """ + import logging import json import time @@ -165,7 +167,8 @@ class RepositoryBuildList(RepositoryParamResource): }, 'docker_tags': { 'type': 'array', - 'description': 'The tags to which the built images will be pushed', + 'description': 'The tags to which the built images will be pushed. ' + + 'If none specified, "latest" is used.', 'items': { 'type': 'string' }, diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index 2481fa807..f1aefa568 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -1,5 +1,8 @@ +""" API discovery information. """ + import re import logging +import sys from flask.ext.restful import reqparse @@ -7,7 +10,7 @@ from endpoints.api import (ApiResource, resource, method_metadata, nickname, tru parse_args, query_param) from app import app from auth import scopes - +from collections import OrderedDict logger = logging.getLogger(__name__) @@ -32,162 +35,195 @@ def fully_qualified_name(method_view_class): def swagger_route_data(include_internal=False, compact=False): - apis = [] + + def swagger_parameter(name, description, kind='path', param_type='string', required=True, + enum=None, schema=None): + # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#parameterObject + parameter_info = { + 'name': name, + 'in': kind, + 'required': required + } + + if not compact: + parameter_info['description'] = description or '' + + if schema: + parameter_info['schema'] = { + '$ref': '#/definitions/%s' % schema + } + else: + parameter_info['type'] = param_type + + if enum is not None and len(list(enum)) > 0: + parameter_info['enum'] = list(enum) + + return parameter_info + + + paths = {} models = {} + tags = [] + tags_added = set() + for rule in app.url_map.iter_rules(): endpoint_method = app.view_functions[rule.endpoint] - if 'view_class' in dir(endpoint_method): - view_class = endpoint_method.view_class + # Verify that we have a view class for this API method. + if not 'view_class' in dir(endpoint_method): + continue - param_data_map = {} - if '__api_path_params' in dir(view_class): - param_data_map = view_class.__api_path_params + view_class = endpoint_method.view_class - operations = [] + # Hide the class if it is internal. + internal = method_metadata(view_class, 'internal') + if not include_internal and internal: + continue - method_names = list(rule.methods.difference(['HEAD', 'OPTIONS'])) - for method_name in method_names: - method = getattr(view_class, method_name.lower(), None) + # Build the tag. + parts = fully_qualified_name(view_class).split('.') + tag_name = parts[-2] + if not tag_name in tags_added: + tags_added.add(tag_name) + tags.append({ + 'name': tag_name, + 'description': (sys.modules[view_class.__module__].__doc__ or '').strip() + }) - parameters = [] + # Build the Swagger data for the path. + swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule) + path_swagger = { + 'name': fully_qualified_name(view_class), + 'tag': tag_name + } - for param in rule.arguments: - parameters.append({ - 'paramType': 'path', - 'name': param, - 'dataType': 'string', - 'description': param_data_map.get(param, {'description': ''})['description'], - 'required': True, - }) + paths[swagger_path] = path_swagger - if method is None: - logger.debug('Unable to find method for %s in class %s', method_name, view_class) - else: - req_schema_name = method_metadata(method, 'request_schema') - if req_schema_name: - parameters.append({ - 'paramType': 'body', - 'name': 'body', - 'description': 'Request body contents.', - 'dataType': req_schema_name, - 'required': True, - }) + # Add any global path parameters. + param_data_map = view_class.__api_path_params if '__api_path_params' in dir(view_class) else {} + if param_data_map: + path_parameters_swagger = [] + for path_parameter in param_data_map: + description = param_data_map[path_parameter].get('description') + path_parameters_swagger.append(swagger_parameter(path_parameter, description)) - schema = view_class.schemas[req_schema_name] - models[req_schema_name] = schema + path_swagger['parameters'] = path_parameters_swagger - if '__api_query_params' in dir(method): - for param_spec in method.__api_query_params: - new_param = { - 'paramType': 'query', - 'name': param_spec['name'], - 'description': param_spec['help'], - 'dataType': TYPE_CONVERTER[param_spec['type']], - 'required': param_spec['required'], - } + # Add the individual HTTP operations. + method_names = list(rule.methods.difference(['HEAD', 'OPTIONS'])) + for method_name in method_names: + # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#operation-object + method = getattr(view_class, method_name.lower(), None) + if method is None: + logger.debug('Unable to find method for %s in class %s', method_name, view_class) + continue - if len(param_spec['choices']) > 0: - new_param['enum'] = list(param_spec['choices']) - - parameters.append(new_param) - - new_operation = { - 'method': method_name, - 'nickname': method_metadata(method, 'nickname') or '(unnamed)' - } - - if not compact: - response_type = 'void' - res_schema_name = method_metadata(method, 'response_schema') - if res_schema_name: - models[res_schema_name] = view_class.schemas[res_schema_name] - response_type = res_schema_name - - new_operation.update({ - 'type': response_type, - 'summary': method.__doc__.strip() if method.__doc__ else '', - 'parameters': parameters, - }) - - - scope = method_metadata(method, 'oauth2_scope') - if scope and not compact: - new_operation['authorizations'] = { - 'oauth2': [ - { - 'scope': scope.scope - } - ], - } - - internal = method_metadata(method, 'internal') - if internal is not None: - new_operation['internal'] = True - - if include_internal: - requires_fresh_login = method_metadata(method, 'requires_fresh_login') - if requires_fresh_login is not None: - new_operation['requires_fresh_login'] = True - - if not internal or (internal and include_internal): - # Swagger requires valid nicknames on all operations. - if new_operation.get('nickname'): - operations.append(new_operation) - else: - logger.debug('Operation missing nickname: %s' % method) - - swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule) - new_resource = { - 'path': swagger_path, - 'description': view_class.__doc__.strip() if view_class.__doc__ else "", - 'operations': operations, - 'name': fully_qualified_name(view_class), + operation_swagger = { + 'operationId': method_metadata(method, 'nickname') or 'unnamed', + 'parameters': [], } - related_user_res = method_metadata(view_class, 'related_user_resource') - if related_user_res is not None: - new_resource['quayUserRelated'] = fully_qualified_name(related_user_res) + if not compact: + operation_swagger.update({ + 'description': method.__doc__.strip() if method.__doc__ else '', + 'tags': [tag_name] + }) - internal = method_metadata(view_class, 'internal') + # Mark the method as internal. + internal = method_metadata(method, 'internal') if internal is not None: - new_resource['internal'] = True + operation_swagger['internal'] = True + if include_internal: + requires_fresh_login = method_metadata(method, 'requires_fresh_login') + if requires_fresh_login is not None: + operation_swagger['requires_fresh_login'] = True + + related_user_res = method_metadata(view_class, 'related_user_resource') + if related_user_res is not None: + operation_swagger['quayUserRelated'] = fully_qualified_name(related_user_res) + + # Add the path parameters. + if rule.arguments: + for path_parameter in rule.arguments: + description = param_data_map.get(path_parameter, {}).get('description') + operation_swagger['parameters'].append(swagger_parameter(path_parameter, description)) + + # Add the query parameters. + if '__api_query_params' in dir(method): + for query_parameter_info in method.__api_query_params: + name = query_parameter_info['name'] + description = query_parameter_info['help'] + param_type = TYPE_CONVERTER[query_parameter_info['type']] + required = query_parameter_info['required'] + + operation_swagger['parameters'].append( + swagger_parameter(name, description, kind='query', + param_type=param_type, + required=required, + enum=query_parameter_info['choices'])) + + # Add the OAuth security block. + # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#securityRequirementObject + scope = method_metadata(method, 'oauth2_scope') + if scope and not compact: + operation_swagger['security'] = [{'oauth2_implicit': scope.scope}] + + # TODO: Add the responses block. + # https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#responsesObject + response_schema_name = method_metadata(method, 'response_schema') + if response_schema_name and not compact: + models[response_schema_name] = view_class.schemas[response_schema_name] + response_type = response_schema_name + + # Add the request block. + request_schema_name = method_metadata(method, 'request_schema') + if request_schema_name and not compact: + models[request_schema_name] = view_class.schemas[request_schema_name] + + operation_swagger['parameters'].append( + swagger_parameter('body', 'Request body contents.', kind='body', + schema=request_schema_name)) + + # Add the operation to the parent path. if not internal or (internal and include_internal): - apis.append(new_resource) + path_swagger[method_name.lower()] = operation_swagger + + tags.sort(key=lambda t: t['name']) + paths = OrderedDict(sorted(paths.items(), key=lambda p: p[1]['tag'])) - # If compact form was requested, simply return the APIs. if compact: - return {'apis': apis} + return {'paths': paths} swagger_data = { - 'apiVersion': 'v1', - 'swaggerVersion': '1.2', - 'basePath': '%s://%s' % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), - 'resourcePath': '/', + 'swagger': '2.0', + 'host': SERVER_HOSTNAME, + 'basePath': '/', + 'schemes': [ + PREFERRED_URL_SCHEME + ], 'info': { - 'title': 'Quay.io API', + 'version': 'v1', + 'title': 'Quay.io Frontend', 'description': ('This API allows you to perform many of the operations required to work ' 'with Quay.io repositories, users, and organizations. You can find out more ' 'at Quay.io.'), - 'termsOfServiceUrl': 'https://quay.io/tos', - 'contact': 'support@quay.io', + 'termsOfService': 'https://quay.io/tos', + 'contact': { + 'email': 'support@quay.io' + } }, - 'authorizations': { - 'oauth2': { - 'scopes': [scope._asdict() for scope in scopes.ALL_SCOPES.values()], - 'grantTypes': { - "implicit": { - "tokenName": "access_token", - "loginEndpoint": { - "url": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), - }, - }, - }, + 'securityDefinitions': { + 'oauth2_implicit': { + "type": "oauth2", + "flow": "implicit", + "authorizationUrl": "%s://%s/oauth/authorize" % (PREFERRED_URL_SCHEME, SERVER_HOSTNAME), + 'scopes': {scope.scope:scope.description for scope in scopes.ALL_SCOPES.values()}, }, }, - 'apis': apis, - 'models': models, + 'paths': paths, + 'definitions': models, + 'tags': tags } return swagger_data diff --git a/endpoints/api/image.py b/endpoints/api/image.py index 939a87d98..a85dce084 100644 --- a/endpoints/api/image.py +++ b/endpoints/api/image.py @@ -1,3 +1,5 @@ +""" List and lookup repository images, and download image diffs. """ + import json from collections import defaultdict diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index d83cff202..a69c129d0 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -1,3 +1,5 @@ +""" Acces usage logs for organizations or repositories. """ + import json from datetime import datetime, timedelta diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 3cb98fb84..849e85c71 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -1,3 +1,5 @@ +""" Manage organizations, members and OAuth applications. """ + import logging from flask import request @@ -333,7 +335,7 @@ def app_view(application): @resource('/v1/organization//applications') @path_param('orgname', 'The name of the organization') class OrganizationApplications(ApiResource): - """ Resource for managing applications defined by an organizations. """ + """ Resource for managing applications defined by an organization. """ schemas = { 'NewApp': { 'id': 'NewApp', diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index 4c0b62074..6e160d688 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -1,3 +1,5 @@ +""" Manage repository permissions. """ + import logging from flask import request diff --git a/endpoints/api/prototype.py b/endpoints/api/prototype.py index de0c97483..d34b3db25 100644 --- a/endpoints/api/prototype.py +++ b/endpoints/api/prototype.py @@ -1,3 +1,5 @@ +""" Manage default permissions added to repositories. """ + from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, diff --git a/endpoints/api/repoemail.py b/endpoints/api/repoemail.py index bb448721a..76c5938f3 100644 --- a/endpoints/api/repoemail.py +++ b/endpoints/api/repoemail.py @@ -1,3 +1,5 @@ +""" Authorize repository to send e-mail notifications. """ + import logging from flask import request, abort diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index bbc1b49de..802a0e36f 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -1,3 +1,5 @@ +""" List, create and manage repositories. """ + import logging import json import datetime diff --git a/endpoints/api/repositorynotification.py b/endpoints/api/repositorynotification.py index 92ea4315c..c5742fd62 100644 --- a/endpoints/api/repositorynotification.py +++ b/endpoints/api/repositorynotification.py @@ -1,3 +1,5 @@ +""" List, create and manage repository events/notifications. """ + import json from flask import request, abort diff --git a/endpoints/api/repotoken.py b/endpoints/api/repotoken.py index a6b28275b..2b64efcd9 100644 --- a/endpoints/api/repotoken.py +++ b/endpoints/api/repotoken.py @@ -1,3 +1,5 @@ +""" Manage repository access tokens (DEPRECATED). """ + import logging from flask import request diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py index 153ea2c1a..b1967f59d 100644 --- a/endpoints/api/robot.py +++ b/endpoints/api/robot.py @@ -1,3 +1,5 @@ +""" Manage user and organization robot accounts. """ + from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource, Unauthorized, require_user_admin, internal_only, require_scope, path_param, parse_args, truthy_bool, query_param) @@ -68,7 +70,6 @@ def robots_list(prefix, include_permissions=False): return {'robots': robots.values()} @resource('/v1/user/robots') -@internal_only class UserRobotList(ApiResource): """ Resource for listing user robots. """ @require_user_admin @@ -85,7 +86,6 @@ class UserRobotList(ApiResource): @resource('/v1/user/robots/') @path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') -@internal_only class UserRobot(ApiResource): """ Resource for managing a user's robots. """ @require_user_admin @@ -181,7 +181,6 @@ class OrgRobot(ApiResource): @resource('/v1/user/robots//permissions') @path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') -@internal_only class UserRobotPermissions(ApiResource): """ Resource for listing the permissions a user's robot has in the system. """ @require_user_admin @@ -222,7 +221,6 @@ class OrgRobotPermissions(ApiResource): @resource('/v1/user/robots//regenerate') @path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') -@internal_only class RegenerateUserRobot(ApiResource): """ Resource for regenerate an organization's robot's token. """ @require_user_admin diff --git a/endpoints/api/search.py b/endpoints/api/search.py index 375be7b77..d8928e84a 100644 --- a/endpoints/api/search.py +++ b/endpoints/api/search.py @@ -1,3 +1,5 @@ +""" Conduct searches against all registry context. """ + from endpoints.api import (ApiResource, parse_args, query_param, truthy_bool, nickname, resource, require_scope, path_param) from data import model diff --git a/endpoints/api/subscribe.py b/endpoints/api/subscribe.py index fc93c330f..501b8e881 100644 --- a/endpoints/api/subscribe.py +++ b/endpoints/api/subscribe.py @@ -1,3 +1,5 @@ +""" Subscribe to plans. """ + import logging import stripe diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index b1152231c..4e84a7c5c 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -1,3 +1,5 @@ +""" Superuser Config API. """ + import logging import os import json diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index c1406dcb8..89f4d4878 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -1,3 +1,5 @@ +""" Superuser API. """ + import string import logging import json diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py index 9ca880e3f..6e7c33a0a 100644 --- a/endpoints/api/tag.py +++ b/endpoints/api/tag.py @@ -1,3 +1,5 @@ +""" Manage the tags of a repository. """ + from flask import request, abort from endpoints.api import (resource, nickname, require_repo_read, require_repo_write, diff --git a/endpoints/api/team.py b/endpoints/api/team.py index ce42f5e94..1dc95b3d1 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -1,3 +1,5 @@ +""" Create, list and manage an organization's teams. """ + from flask import request from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 8706ccf31..727f97731 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -1,3 +1,5 @@ +""" Create, list and manage build triggers. """ + import json import logging @@ -397,7 +399,7 @@ class ActivateBuildTrigger(RepositoryParamResource): 'properties': { 'branch_name': { 'type': 'string', - 'description': '(GitHub Only) If specified, the name of the GitHub branch to build.' + 'description': '(SCM only) If specified, the name of the branch to build.' }, 'commit_sha': { 'type': 'string', diff --git a/endpoints/api/user.py b/endpoints/api/user.py index b03c5f87b..f1bb66bbe 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -1,3 +1,5 @@ +""" Manage the current user. """ + import logging import json @@ -182,11 +184,17 @@ class User(ApiResource): }, 'organizations': { 'type': 'array', - 'description': 'Information about the organizations in which the user is a member' + 'description': 'Information about the organizations in which the user is a member', + 'items': { + 'type': 'object' + } }, 'logins': { 'type': 'array', - 'description': 'The list of external login providers against which the user has authenticated' + 'description': 'The list of external login providers against which the user has authenticated', + 'items': { + 'type': 'object' + } }, 'can_create_repo': { 'type': 'boolean', @@ -794,10 +802,10 @@ class StarredRepositoryList(ApiResource): @path_param('repository', 'The full path of the repository. e.g. namespace/name') class StarredRepository(RepositoryParamResource): """ Operations for managing a specific starred repository. """ - @nickname('deleteStar') @require_user_admin def delete(self, namespace, repository): + """ Removes a star from a repository. """ user = get_authenticated_user() repo = model.get_repository(namespace, repository) From 3fb2a33ee7484528941a4bcfb6aafd7369574084 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 14 May 2015 17:15:39 -0400 Subject: [PATCH 2/2] Fix the API service to use the new Swagger description form --- endpoints/api/discovery.py | 10 +++-- endpoints/api/logs.py | 2 +- endpoints/api/robot.py | 1 + static/js/services/api-service.js | 65 ++++++++++++++++--------------- templates/base.html | 2 +- 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/endpoints/api/discovery.py b/endpoints/api/discovery.py index f1aefa568..210410996 100644 --- a/endpoints/api/discovery.py +++ b/endpoints/api/discovery.py @@ -94,9 +94,15 @@ def swagger_route_data(include_internal=False, compact=False): swagger_path = PARAM_REGEX.sub(r'{\2}', rule.rule) path_swagger = { 'name': fully_qualified_name(view_class), + 'path': swagger_path, 'tag': tag_name } + if include_internal: + related_user_res = method_metadata(view_class, 'related_user_resource') + if related_user_res is not None: + path_swagger['quay_user_related'] = fully_qualified_name(related_user_res) + paths[swagger_path] = path_swagger # Add any global path parameters. @@ -139,10 +145,6 @@ def swagger_route_data(include_internal=False, compact=False): if requires_fresh_login is not None: operation_swagger['requires_fresh_login'] = True - related_user_res = method_metadata(view_class, 'related_user_resource') - if related_user_res is not None: - operation_swagger['quayUserRelated'] = fully_qualified_name(related_user_res) - # Add the path parameters. if rule.arguments: for path_parameter in rule.arguments: diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index a69c129d0..55c6110ad 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -1,4 +1,4 @@ -""" Acces usage logs for organizations or repositories. """ +""" Access usage logs for organizations or repositories. """ import json diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py index b1967f59d..b64eb0cca 100644 --- a/endpoints/api/robot.py +++ b/endpoints/api/robot.py @@ -70,6 +70,7 @@ def robots_list(prefix, include_permissions=False): return {'robots': robots.values()} @resource('/v1/user/robots') +@internal_only class UserRobotList(ApiResource): """ Resource for listing user robots. """ @require_user_admin diff --git a/static/js/services/api-service.js b/static/js/services/api-service.js index 5952463fd..10c8b137f 100644 --- a/static/js/services/api-service.js +++ b/static/js/services/api-service.js @@ -101,27 +101,14 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService' var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) { if (userRelatedResource) { - var operations = userRelatedResource['operations']; - for (var i = 0; i < operations.length; ++i) { - var operation = operations[i]; - if (operation['method'].toLowerCase() == method) { - return operation['nickname']; - } + if (userRelatedResource[method.toLowerCase()]) { + return userRelatedResource[method.toLowerCase()]['operationId']; } } throw new Error('Could not find user operation matching org operation: ' + orgOperationName); }; - var buildMethodsForEndpointResource = function(endpointResource, resourceMap) { - var name = endpointResource['name']; - var operations = endpointResource['operations']; - for (var i = 0; i < operations.length; ++i) { - var operation = operations[i]; - buildMethodsForOperation(operation, endpointResource, resourceMap); - } - }; - var freshLoginInProgress = []; var reject = function(msg) { for (var i = 0; i < freshLoginInProgress.length; ++i) { @@ -224,14 +211,13 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService' }; }; - var buildMethodsForOperation = function(operation, resource, resourceMap) { - var method = operation['method'].toLowerCase(); - var operationName = operation['nickname']; - var path = resource['path']; + var buildMethodsForOperation = function(operation, method, path, resourceMap) { + var operationName = operation['operationId']; + var urlPath = path['path']; // Add the operation itself. apiService[operationName] = function(opt_options, opt_parameters, opt_background, opt_forcessl) { - var one = Restangular.one(buildUrl(path, opt_parameters, opt_forcessl)); + var one = Restangular.one(buildUrl(urlPath, opt_parameters, opt_forcessl)); if (opt_background) { one.withHttpConfig({ 'ignoreLoadingBar': true @@ -251,15 +237,15 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService' // If the method for the operation is a GET, add an operationAsResource method. if (method == 'get') { apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) { - return getResource(buildUrl(path, opt_parameters), opt_background); + return getResource(buildUrl(urlPath, opt_parameters), opt_background); }; } - // If the resource has a user-related resource, then make a generic operation for this operation + // If the operation has a user-related operation, then make a generic operation for this operation // that can call both the user and the organization versions of the operation, depending on the // parameters given. - if (resource['quayUserRelated']) { - var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[resource['quayUserRelated']]); + if (path['quay_user_related']) { + var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[path['quay_user_related']]); var genericOperationName = getGenericOperationName(userOperationName); apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) { if (orgname) { @@ -280,19 +266,34 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService' return apiService; } + var allowedMethods = ['get', 'post', 'put', 'delete']; var resourceMap = {}; + var forEachOperation = function(callback) { + for (var path in window.__endpoints) { + if (!window.__endpoints.hasOwnProperty(path)) { + continue; + } + + for (var method in window.__endpoints[path]) { + if (!window.__endpoints[path].hasOwnProperty(method)) { + continue; + } + + if (allowedMethods.indexOf(method.toLowerCase()) < 0) { continue; } + callback(window.__endpoints[path][method], method, window.__endpoints[path]); + } + } + }; // Build the map of resource names to their objects. - for (var i = 0; i < window.__endpoints.length; ++i) { - var endpointResource = window.__endpoints[i]; - resourceMap[endpointResource['name']] = endpointResource; - } + forEachOperation(function(operation, method, path) { + resourceMap[path.name] = path; + }); // Construct the methods for each API endpoint. - for (var i = 0; i < window.__endpoints.length; ++i) { - var endpointResource = window.__endpoints[i]; - buildMethodsForEndpointResource(endpointResource, resourceMap); - } + forEachOperation(function(operation, method, path) { + buildMethodsForOperation(operation, method, path, resourceMap); + }); apiService.getErrorMessage = function(resp, defaultMessage) { var message = defaultMessage; diff --git a/templates/base.html b/templates/base.html index 91dee7842..77f28c2a0 100644 --- a/templates/base.html +++ b/templates/base.html @@ -41,7 +41,7 @@ {% endblock %}