-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat(openapi-v3): responses decorator #1655
Conversation
description: 'Todo model instance', | ||
content: {'application/json': {schema: Todo}}, | ||
}, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. Very intuitive to folks who are familiar with OpenAPI spec.
@responses({ | ||
'200': { | ||
description: 'Put success', | ||
content: {'application/json': {schema: {type: 'boolean'}}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might want to allow {schema: Boolean}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may need story #1112 to happen first.
'responses schema of type array detected with items as a model / function', | ||
); | ||
operationSpec.responses[code].content[c].schema.items = getModelRef( | ||
operationSpec.responses[code].content[c].schema.items, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactor operationSpec.responses[code].content[c].schema
to be a variable.
@@ -239,3 +285,7 @@ export function getControllerSpec(constructor: Function): ControllerSpec { | |||
} | |||
return spec; | |||
} | |||
|
|||
function getModelRef(fn: Function) { | |||
return {$ref: `#/components/schemas/${fn.name}`}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If fn
is a Model class with @model({name: 'my-model'})
, we should use my-model
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if we could share the implementation of "Model class to $ref" algorithm with openapi-v3 package, see https://github.com/strongloop/loopback-next/blob/8499862ed7bbb615d8bcdf2c2590fcdfaa624e8f/packages/openapi-v3/src/generate-schema.ts#L54-L65
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 LGTM so far. Just a few nitpicks.
@@ -0,0 +1,100 @@ | |||
// Copyright IBM Corp. 2018. All Rights Reserved. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could add this file in https://github.com/strongloop/loopback-next/blob/master/packages/openapi-v3/docs.json so the tsdoc generates API doc for it :)
descriptor: TypedPropertyDescriptor<any>, | ||
) { | ||
debug('@responses() on %s.%s', target.constructor.name, member); | ||
debug(' responsesObject: %s', inspect(responsesObject, {depth: null})); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: is the space before responsesObject
intended?
|
||
debug(' decorated responses for method %s: %o', op, responses); | ||
|
||
const defaultResponse = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: how about declare it before Line97 to reuse the defaultResponse
in
if (!operationSpec) {
// The operation was defined via @operation(verb, path) with no spec
operationSpec = {
responses: defaultResponse
};
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I commented earlier, I am proposing to remove @responses
decorator from this pull request.
If we decide to keep it around, then I agree too that the default response spec should be reused between these two places.
@responses({ | ||
'200': { | ||
description: 'Put success', | ||
content: {'application/json': {schema: {type: 'boolean'}}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may need story #1112 to happen first.
The proposed decorator API looks great for advanced users. I have a question though - how is Current proposal: @post('/todos')
@responses({
'200': {
description: 'Todo model instance',
content: {'application/json': {schema: Todo}},
},
}) Existing solution working OOTB right now: @post('/todos', {
responses: {
description: 'Todo model instance',
content: {'application/json': {schema: Todo}},
},
}) } When it comes to adding a new decorator API, I would like to see a simpler API that will hide away the complexity of OpenAPI format and allow developers to focus on the important parts only. For example: @post()
@returnsJson(Todo, 'Todo model instance')
async createTodo(@requestBody() todo: Todo): Promise<Todo> {
// ...
} I picked
I am proposing to remove
I am not sure whether schema inference is usable. IIUC, we are not able to infer On the other hand, if we are able to infer the schema from existing controller methods in our examples, then I would expect that no changes to our examples and CLI templates should be needed? Either way, it seems to me that we need to implement only one of these two changes. Thoughts? UPDATE
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see my comment above first, it explains why I disagree with the current direction and offers a different approach instead.
Besides the comments below, this PR is also missing an update of CLI templates, tutorial docs and general documentation related to writing Controllers. You are probably aware of that already, but the pull request description does not make it clear.
@@ -120,7 +120,7 @@ export class OpenApiSpecBuilder extends BuilderBase<OpenApiSpec> { | |||
export class OperationSpecBuilder extends BuilderBase<OperationObject> { | |||
constructor() { | |||
super({ | |||
responses: {}, | |||
responses: {'200': {description: ''}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use a non-empty string please. For example description: 'An undocumented response body.'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL, this comment hasn't been addressed yet.
Let's use a non-empty string please. For example description: 'An undocumented response body.'
responses: {}, | ||
responses: { | ||
'200': { | ||
description: '', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use a non-empty default value please. For example, we can use the controller name and method name to build a more descriptive text:
description: `Return value of controller method ${fullMethodName}`
|
||
debug(' decorated responses for method %s: %o', op, responses); | ||
|
||
const defaultResponse = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I commented earlier, I am proposing to remove @responses
decorator from this pull request.
If we decide to keep it around, then I agree too that the default response spec should be reused between these two places.
@@ -239,3 +285,7 @@ export function getControllerSpec(constructor: Function): ControllerSpec { | |||
} | |||
return spec; | |||
} | |||
|
|||
function getModelRef(fn: Function) { | |||
return {$ref: `#/components/schemas/${fn.name}`}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be great if we could share the implementation of "Model class to $ref" algorithm with openapi-v3 package, see https://github.com/strongloop/loopback-next/blob/8499862ed7bbb615d8bcdf2c2590fcdfaa624e8f/packages/openapi-v3/src/generate-schema.ts#L54-L65
Please note that Here is an idea I have to mitigate the issue:
@schemas({Customer, MyOrder: Order})
export class MyController {} Or a @schema(Customer)
async findById(id: string) {}
async create(@schema(Customer) customer: DeepPartial<Customer>) {} With this approach, we can have an OpenAPI compatible spec for parameters and responses while allowing developers to map TS types to schema definitions. In the long run, we can possibly use |
I had an offline chat with @virkt25 and we think the PR can move forward as follows:
|
Ah, good catch about
The plan sound good to me 👍 Let's pick a different name than On the other hand, how about introducing a helper function that can convert a class into a schema value? I think that will be easier to use and give us more flexibility for the future. import {modelRef} from '@loopback/openapi-v3'; // or from @loopback/rest
@post('/todos', {
responses: {
description: 'Todo model instance',
content: {'application/json': {schema: modelRef(Todo)}},
},
}) Under the hood,
|
+1 to have
|
0dbf2e1
to
4391523
Compare
UPDATED: This PR:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The patch looks pretty good at high level now.
I have few comments related to implementation details, see below.
responses: { | ||
'200': { | ||
description: 'TodoList.Todo model instance', | ||
content: {'application/json': {'x-ts-type': Todo}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just in case it was not clear, if we added modelRef(ctor)
helper as a sugar API returning {'x-ts-type': ctor}
, then the line above could look like this:
content: {'application/json': modelRef(Todo)},
Having wrote that, I am not sure how much better that is and whether the benefits justify maintenance costs. I guess what we really want is to implement the new decorator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather see new decorators than introduce a helper utility that we have to maintain.
responses: { | ||
'200': { | ||
description: 'TodoList.Todo PATCH success count', | ||
content: {'application/json': {'x-ts-type': Number}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, a response returning just a number is not very usual in REST/JSON world, is it?
I think it would be better to return an object with a count
property instead, such response is easier to extend with additional properties while preserving backwards compatibility.
Having said that, such change is out of scope of this pull request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's unusual but it's what our Repository returns. I'll make a follow up issue for us to return an object instead.
@@ -120,7 +120,7 @@ export class OpenApiSpecBuilder extends BuilderBase<OpenApiSpec> { | |||
export class OperationSpecBuilder extends BuilderBase<OperationObject> { | |||
constructor() { | |||
super({ | |||
responses: {}, | |||
responses: {'200': {description: ''}}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL, this comment hasn't been addressed yet.
Let's use a non-empty string please. For example description: 'An undocumented response body.'
const TS_TYPE_KEY = 'x-ts-type'; | ||
|
||
for (const code in operationSpec.responses) { | ||
for (const c in operationSpec.responses[code].content) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIRC, content
is an optional property. Please add a test to verify what happens when the OAI spec does not provide any value for content
, and fix the implementation if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
content
is an optional property and definitely took me a minute to realize what was happening here since I coded it.
The for (const c in operationSpec.responses[code].content)
loop is only entered if the operationSpec.responses[code]
object had a content
property. If the property is not defined, the loop is never entered as there are no properties on undefined
-- making the implementation correct and sound.
I also verified this behaviour on this page: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
By changing the code to be as follows:
var string1 = "";
var object1 = {a: 1, b: 2, c: 3};
// var object1 = {a: 1, b: 2, c: 3, d: {x:1, y:2}};
for (var property1 in object1.d) {
console.log('in loop')
string1 = string1 + property1;
}
console.log(string1);
// expected output: "123"
The above will log ""
as is and uncommenting line 3 should give "xy"
after 2x in loop
for (const code in operationSpec.responses) { | ||
for (const c in operationSpec.responses[code].content) { | ||
debug(' evaluating response code %s with content: %o', code, c); | ||
const tsType = operationSpec.responses[code].content[c][TS_TYPE_KEY]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please extract operationSpec.responses[code].content[c]
into a shared variable to avoid repetition.
responses: { | ||
'200': { | ||
description: 'Return value of FooController.create', | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. Is this the test verifying how a response status with no content
is handled?
IIRC,
content
is an optional property. Please add a test to verify what happens when the OAI spec does not provide any value forcontent
, and fix the implementation if needed.
It makes me wonder why this test passes, when the code processing the schema is not checking whether .content
is set. Are we filling the missing content
property somewhere else?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #1655 (comment)
4391523
to
c27b10b
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
Please clean up the commit history before landing.
7534e00
to
eda80ca
Compare
Co-Authored-By: Raymond Feng <[email protected]>
eda80ca
to
f9309a5
Compare
connected to #1531
This PR:
responses
property on a Controller methods OpenAPI Spec if one isn't givenx-ts-type
property that can be used to specify the schema of a response object. Can also be used to specify the type of array items.generate-schema
to have a single function to resolve schema from a Function.responses
object based on the Modelexample-todo
/example-todo-list
to setresponses
objectThis PR introduces:- a new@responses
decorator that can be used to decorate a Controller method with the appropriate OpenAPI responses object.- for controller's not decorated with this property, or via the@operation
and family (get, post, etc.) of decorators, we generate a default responses object to be OpenAPI 3.0 compliant-controller-spec
generator can set Model as$ref
for{schema: Model}
or {schema: {type: 'array', items: Model}}}in the responses object.~ ~- Decorate
example-todowith
@Responses()` decorator as an example. Once this PR is agreed upon, a follow up PR/commit will add decoration to CLI template + remaining examples + docsChecklist
npm test
passes on your machinepackages/cli
were updatedexamples/*
were updated