encode / apistar

The Web API toolkit. 🛠
https://docs.apistar.com
BSD 3-Clause "New" or "Revised" License
5.57k stars 411 forks source link

Selecting a server or setting a custom endpoint #657

Open Lucidiot opened 5 years ago

Lucidiot commented 5 years ago

OpenAPI 3 allows for multiple Server Objects to be defined on a single document; however, it is not currently possible to select which server is wanted with the APIStar client, nor it is to set a custom base URL, except with setting the URL on the schema document itself and updating every Link URL.

I guess adding a base_url=... keyword argument to apistar.Client isn't hard, but how should one select an existing server from the schema, or list them? Currently, the URL from the first server is used: https://github.com/encode/apistar/blob/master/apistar/schemas/openapi.py#L357

tomchristie commented 5 years ago

Yup - not really sure just yet.

Thanks for digging into all this so much!

I've a bit pushed for time right now so might not be reviewing apistar tickets for the next few days, but I'll get back onto it in due course.

MattFisher commented 5 years ago

I just ran into this too.

According to the spec for the paths object, the path should be appended to the server object's url

The field name MUST begin with a slash. The path is appended (no relative URL resolution) to the expanded URL from the Server Object's url field in order to construct the full URL.

but OpenAPI.get_link uses urljoin to set the Link's url, which strips any path (like /api/v1) off the url that comes from the server object. https://github.com/encode/apistar/blob/master/apistar/schemas/openapi.py#L465

pjxiao commented 4 years ago

+1

I tried to avoid this by stripping leading / in paths as follows, but this caused typesystem.base.ValidationError.

doc['paths'] = {
    path.lstrip('/'): path_item_obj
    for path, path_item_obj in doc['paths'].items()
}
apistar.Client(doc)

This is because properties of Paths object are required leading /. https://github.com/encode/apistar/blob/2edeb694/apistar/schemas/openapi.py#L102

Lucidiot commented 4 years ago

The required leading slash is a requirement from OpenAPI itself (see the Paths Object specs).

Here is the workaround we used:

from urllib.parse import urlsplit

class MyClient(apistar.Client):

    def __init__(self, *args, base_url=None, **kwargs):
        # Let APIStar do its parsing
        super().__init__(*args, **kwargs)

        # Strip scheme, hostname and absolute path from all link URLs
        for link_info in self.document.walk_links():
            original_url = urlsplit(link_info.link.url)
            new_url = ('', '', *original_url[2:])
            link_info.link.url = urlunsplit(new_url).lstrip('/')

        if base_url:
            # Ensure the base URL ends with a slash to prevent issues:
            # urljoin('http://a/b', 'c') → http://a/c
            # urljoin('http://a/b/', 'c') → http://a/b/c
            if not base_url.endswith('/'):
                base_url += '/'

            self.document.base_url = base_url

This goes through every parsed link in the Document instance to remove the scheme, hostname and leading slash, resulting in every API endpoint having a relative URL (a/b/c instead of http://myserver.com/a/b/c), then updates Document.url to a custom base URL when specified. This then allows Client.get_url to still use urljoin and do any of its checks, but with any base URL, including custom base paths.