Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support $ref from endpoint response to components/responses BNCH-109662 #213

Merged
merged 7 commits into from
Oct 28, 2024

Conversation

eli-bl
Copy link

@eli-bl eli-bl commented Oct 24, 2024

Fixes openapi-generators#1125.

I've also opened a PR from the same branch in the upstream repo (here), but here I'm targeting it to our fork repo in against a new 2.x branch (which I created by pulling the latest release code on main from the upstream repo). The main branch in our fork repo is still the same as it was, that is, a heavily modified fork of a very old version of the upstream repo.

See also: https://benchling.atlassian.net/wiki/spaces/AE/pages/730268159/Fixes+necessary+to+use+openapi-python-client+for+v3+API

Why we need this fix: The V3 API spec makes heavy use of reusable response definitions like this, so that we don't have to spell out the same standard error responses for every endpoint.

This makes it so the following—

components:
  responses:
    GoodResponse:
      description: OK
      content:
        "application/json":
          schema:
            $ref: "#/components/schemas/MyObject"

paths:
  "/endpoint":
    get:
      responses:
        "200":
          $ref: "#/components/responses/GoodResponse"

—is exactly equivalent to:

paths:
  "/endpoint":
    get:
      responses:
        "200":
          description: OK
          content:
            "application/json":
              schema:
                $ref: "#/components/schemas/MyObject"

Previously, it would have failed to follow the response reference and behaved as if the response had no content.

This still requires the response to be fully defined inline in components/responses; it can't itself consist of just a $ref.

@@ -991,6 +991,20 @@
}
}
},
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of the files under end_to_end_tests can be ignored - they're just example data that's used by a test that runs the generator against a big spec file and checks for the expected output files.

@eli-bl eli-bl changed the title fix: support $ref from endpoint response to components/responses fix: support $ref from endpoint response to components/responses BNCH-109662 Oct 25, 2024
Comment on lines 99 to 105
if not ref_path.startswith("/components/responses/"):
return ParseError(data=data, detail=f"$ref to {data.ref} not allowed in responses"), schemas
resp_data = responses.get(ref_path.split("/")[-1], None)
if not resp_data:
return ParseError(data=data, detail=f"Could not find reference: {data.ref}"), schemas
if not isinstance(resp_data, oai.Response):
return ParseError(data=data, detail="Top-level $ref inside components/responses is not supported"), schemas

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of individual return statements like these,... what if we used a error_detail: str variable and just have one return statement at the end of this function that has one return statement for ParseError -- this may or may not look very pretty but at least it would avoid disabling PLR0911 rule

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly find it easier to follow the logic when there's explicit short-circuiting like this. Maybe it's because I did a lot of Go and that's the preferred pattern in Go. But if I see a bunch of this kind of thing—

if bad_condition_1:
    error_detail = message1
elif bad_condition_2:
    error_detail = message2
else:
    error_detail = None
if error_detail:
    return ParseError(data=data, detail=error_detail)

—it bothers me a little because now the code paths for the error conditions are not really independent of each other; if for instance I messed up and put "if" instead of "elif" for the second one, now the bad_condition_1 test would be effectively a no-op. It also means that I have to be constructing the same kind of error, ParseError, each time (and the same way each time: data=data), which happens to be true in this case but isn't logically inevitable; if I had to make a change where now one of those cases raises a different error, I'd have to refactor the whole thing.

PLR0911 is only a helpful rule when it's helpful, and this codebase disables it in quite a few parsing/validation functions like this.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PLR0911 is only a helpful rule when it's helpful, and
agree

this codebase disables it in quite a few parsing/validation functions like this

okay this sounds good to me

return ref_path, schemas
if not ref_path.startswith("/components/responses/"):
return ParseError(data=data, detail=f"$ref to {data.ref} not allowed in responses"), schemas
resp_data = responses.get(ref_path.split("/")[-1], None)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little surprised that getting reference data is not a little more standardized or at least with a helper.... is doing a split and -1 the general pattern in the rest of the repo?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no use case in the rest of the repo for getting just the type name from a reference.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this isn't really "getting reference data". The helper is just converting the string "#/components/schemas/MyTypeName" to the string "MyTypeName".

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, I was wrong, there are a couple other usages like this. So I've added a helper.

*,
status_code: HTTPStatus,
data: Union[oai.Response, oai.Reference],
schemas: Schemas,
responses: Dict[str, Union[oai.Response, oai.Reference]],

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a breaking change to this function's signature....What's the general methodology to validate that this is safe (possibly other places that might call this function?)
I think we should have good confidence from passing tests but just calling it out in case there's anything additional we need to consider

Copy link
Author

@eli-bl eli-bl Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's validated by the existing unit tests for the repo, which include 1. a bunch of tests of building an Endpoint (which is literally the only code path response_from_data is called from), and 2. a full end-to-end test of running the generator on an entire API spec that includes endpoints with responses.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

building an Endpoint (which is literally the only code path response_from_data is called from

oh okayy cool... this is what I wanted to confirm!

Comment on lines +49 to +55
def get_reference_simple_name(ref_path: str) -> str:
"""
Takes a path like `/components/schemas/NameOfThing` and returns a string like `NameOfThing`.
"""
return ref_path.split("/", 3)[-1]


Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍

@eli-bl eli-bl merged commit 23b6caf into 2.x Oct 28, 2024
@eli-bl eli-bl deleted the responses-by-ref branch October 29, 2024 22:21
@eli-bl eli-bl restored the responses-by-ref branch October 29, 2024 22:21
@eli-bl eli-bl deleted the responses-by-ref branch November 20, 2024 23:29
@eli-bl eli-bl restored the responses-by-ref branch November 20, 2024 23:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants