{"action":"created","issue":{"url":"https://api.github.com/repos/box/ClusterRunner/issues/381","repository_url":"https://api.github.com/repos/box/ClusterRunner","labels_url":"https://api.github.com/repos/box/ClusterRunner/issues/381/labels{/name}","comments_url":"https://api.github.com/repos/box/ClusterRunner/issues/381/comments","events_url":"https://api.github.com/repos/box/ClusterRunner/issues/381/events","html_url":"https://github.com/box/ClusterRunner/pull/381","id":240821088,"number":381,"title":"Make the v1 in URIs optional","user":{"login":"nickzuber","id":10540865,"avatar_url":"https://avatars2.githubusercontent.com/u/10540865?v=3","gravatar_id":"","url":"https://api.github.com/users/nickzuber","html_url":"https://github.com/nickzuber","followers_url":"https://api.github.com/users/nickzuber/followers","following_url":"https://api.github.com/users/nickzuber/following{/other_user}","gists_url":"https://api.github.com/users/nickzuber/gists{/gist_id}","starred_url":"https://api.github.com/users/nickzuber/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/nickzuber/subscriptions","organizations_url":"https://api.github.com/users/nickzuber/orgs","repos_url":"https://api.github.com/users/nickzuber/repos","events_url":"https://api.github.com/users/nickzuber/events{/privacy}","received_events_url":"https://api.github.com/users/nickzuber/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":1,"created_at":"2017-07-06T01:39:37Z","updated_at":"2017-07-06T20:30:52Z","closed_at":null,"pull_request":{"url":"https://api.github.com/repos/box/ClusterRunner/pulls/381","html_url":"https://github.com/box/ClusterRunner/pull/381","diff_url":"https://github.com/box/ClusterRunner/pull/381.diff","patch_url":"https://github.com/box/ClusterRunner/pull/381.patch"},"body":"The `v1` in the URIs is now flagged as an optional route. This means that it may or may not be included in any given endpoint. For example:\r\n\r\n```\r\nhttp://localhost:43000/build\r\n\r\n↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ \r\n\r\n{\r\n    \"builds\": [],\r\n    \"child_routes\": {\r\n        \"build\": \"/build/[build_id]\"\r\n    }\r\n}\r\n```\r\nand\r\n```\r\nhttp://localhost:43000/v1/build\r\n\r\n↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ \r\n\r\n{\r\n    \"builds\": [],\r\n    \"child_routes\": {\r\n        \"build\": \"/v1/build/[build_id]\"\r\n    }\r\n}\r\n```\r\n\r\nWhat's new\r\n------------\r\n\r\n### Getting child routes\r\n\r\nAlso there's a new method in the `RouteNode` class called `get_child_routes` which returns the children of a given `RouteNode`. The difference from using this method and simply calling `route_node.children` is in `get_child_routes`, if a child route is flagged as optional and it _does not_ show up in the request URI, then we consider its children to be part of the current route's children. \r\n\r\n```python\r\ndef get_child_routes(self, uri=None):\r\n    \"\"\"\r\n    Get a list of all children routes for this route. If one of the child routes is marked optional\r\n    and it doesn't appear within the requested URI, we return its children instead of it.\r\n    :param uri: The URI of the request.\r\n    \"\"\"\r\n    if uri is None:\r\n        return self.children\r\n\r\n    uri_parts = uri.split('/')\r\n    child_routes = list()\r\n    for child in self.children:\r\n        if child.optional and child.name() not in uri_parts:\r\n            child_routes += child.get_child_routes(uri)\r\n        else:\r\n            child_routes.append(child)\r\n    return child_routes\r\n```\r\n\r\nExample of this affecting the `/v1/` endpoint:\r\n\r\n```\r\nhttp://localhost:43000/v1\r\n\r\n↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ \r\n\r\n{\r\n    \"child_routes\": {\r\n        \"slaves\": \"/v1/slave\",\r\n        \"eventlog\": \"/v1/eventlog\",\r\n        \"version\": \"/v1/version\",\r\n        \"metrics\": \"/v1/metrics\",\r\n        \"queue\": \"/v1/queue\",\r\n        \"builds\": \"/v1/build\"\r\n    },\r\n    \"master\": {\r\n        \"slaves\": [],\r\n        \"status\": \"Master service is up.\"\r\n    }\r\n}\r\n```\r\n\r\nand without `v1` in the URI\r\n\r\n```\r\nhttp://localhost:43000/\r\n\r\n↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ \r\n\r\n{\r\n    \"child_routes\": {\r\n        \"slaves\": \"/slave\",\r\n        \"eventlog\": \"/eventlog\",\r\n        \"version\": \"/version\",\r\n        \"metrics\": \"/metrics\",\r\n        \"queue\": \"/queue\",\r\n        \"builds\": \"/build\"\r\n    }\r\n}\r\n```\r\n\r\n### Route templates\r\n\r\nRoute templates now handle routes that are marked as optional. If a route is marked as optional and _does not_ appear in the request URI, then it is not considered part of the route template.\r\n\r\n```python\r\ndef route_template(self, uri=None):\r\n    \"\"\"\r\n    The generic form of this route, for display in the API 'child routes'\r\n    :rtype: str\r\n    \"\"\"\r\n    uri_parts = uri.split('/') if uri else []\r\n    ancestor_names = []\r\n    for ancestor in list(reversed(self.ancestors())):\r\n        # If a route is marked optional, only include it in route template if its already in URI\r\n        if ancestor.optional and ancestor.name() not in uri_parts:\r\n            continue\r\n        ancestor_names.append(ancestor.name().rstrip('/'))\r\n\r\n    return '/'.join(ancestor_names + [self.name()])\r\n```\r\n\r\n### Building URLs\r\n\r\nThe `UrlBuilder` now has an optional parameter that dictates whether or not the URL being build will include the `v1`. By default it _does_ include `v1` for backwards compatibility.\r\n\r\n```python\r\ndef url(self, *args, use_versioned_url=True)\r\n```\r\n\r\nNotes\r\n------\r\n\r\nThe `RouteHandler` associated with the `v1` endpoint writes some information about the master service:\r\n\r\n```python\r\ndef get(self):\r\n    response = {\r\n        'master': self._cluster_master.api_representation(),\r\n    }\r\n    self.write(response)\r\n```\r\n\r\nSince this route is optional, we should think about include this behavior to the root `RequestHandler` as well or something. Right now the only way to get this information is to hit the `/v1/` endpoint, which shouldn't be the case if we don't necessarily want people going out of their way to use `v1`.\r\n\r\n"},"comment":{"url":"https://api.github.com/repos/box/ClusterRunner/issues/comments/313510683","html_url":"https://github.com/box/ClusterRunner/pull/381#issuecomment-313510683","issue_url":"https://api.github.com/repos/box/ClusterRunner/issues/381","id":313510683,"user":{"login":"josephharrington","id":1109303,"avatar_url":"https://avatars0.githubusercontent.com/u/1109303?v=3","gravatar_id":"","url":"https://api.github.com/users/josephharrington","html_url":"https://github.com/josephharrington","followers_url":"https://api.github.com/users/josephharrington/followers","following_url":"https://api.github.com/users/josephharrington/following{/other_user}","gists_url":"https://api.github.com/users/josephharrington/gists{/gist_id}","starred_url":"https://api.github.com/users/josephharrington/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/josephharrington/subscriptions","organizations_url":"https://api.github.com/users/josephharrington/orgs","repos_url":"https://api.github.com/users/josephharrington/repos","events_url":"https://api.github.com/users/josephharrington/events{/privacy}","received_events_url":"https://api.github.com/users/josephharrington/received_events","type":"User","site_admin":false},"created_at":"2017-07-06T20:30:52Z","updated_at":"2017-07-06T20:30:52Z","body":"I made some comments on your #380 that might affect this change. 😬"}}
{"actor":{"display_login":"josephharrington"}}