Skip to content

Commit

Permalink
feat: API-28061 - update example request body generation (#150)
Browse files Browse the repository at this point in the history
* feat(positive suite): aPI-28061 - Update request body generation

Create example request body from MediaTypeObject.example if present. Create a request body using
only required fields as well as the entire example.

* refactor: aPI-28061 - refactor request body building

* test: aPI-28061 - update tests and add new tests

* refactor: aPI-28061 - rename required fields request body constant

* refactor: aPI-28061 - refactor OperationExampleFactory and tests

* refactor(operationexamplefactory): aPI-28061 - update operation-example.factory.ts logic

* docs: aPI-28061 - add comments to RequestBodyFactory and OperationExampleFactory

* ci(jenkinsfile): aPI-28061 - test using ghcr DockerImage for building LOAST

* docs(readme): aPI-28016 - update README Example Request Body section

* revert(jenkinsfile): aPI-28061 - undo Jenkinsfile changes
  • Loading branch information
christineyost authored Aug 17, 2023
1 parent 78673c1 commit 9b0313b
Show file tree
Hide file tree
Showing 21 changed files with 749 additions and 155 deletions.
191 changes: 148 additions & 43 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -538,58 +538,162 @@ Example Groups are built from Parameter objects in both the Path Item Object and

</details>

## Example Request Body
## Example Request Bodies

An Example Request Body is built from Property objects in the Request Body's Content schema.
All properties listed as required will be included in the example request body.
Properties that are not required will not be included, even if a property example is provided.
If an Operation requires a request body, LOAST will build a default ExampleRequestBody using an OAS Operation source in order of precedence:

<details><summary>Sample JSON</summary>
- MediaTypeObject.example
- The "example" field on each property in MediaTypeObject.schema.properties

The default ExampleRequestBody will include every field included in MediaTypeObject.example or every property in MediaTypeObject.schema.properties that has its "example" field set.

If the default ExampleRequestBody contains optional fields, then an additional required-fields-only ExampleRequestBody will be constructed. This request body will only contain fields that are marked as required in MediaTypeObject.schema.

<details><summary>Sample JSON - MediaTypeObject.example</summary>

```json
"schemas": {
"VeteranStatusRequest": {
"type": "object",
"required": [
"ssn",
"first_name",
"last_name",
"birth_date"
],
"properties": {
"ssn": {
"type": "string",
"example": "555-55-5555"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"birth_date": {
"type": "string",
"example": "1965-01-01"
},
"middle_name": {
"type": "string",
"example": "Theodore"
},
"gender": {
"type": "string",
"enum": [
"M",
"F"
],
"example": "M"
"paths": {
"/status": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ssn",
"first_name",
"last_name",
"birth_date"
],
"properties": {
"ssn": {
"type": "string"
},
"first_name": {
"type": "string"
},
"last_name": {
"type": "string"
},
"birth_date": {
"type": "string"
},
"middle_name": {
"type": "string"
},
"gender": {
"type": "string",
"enum": [
"M",
"F"
]
}
}
},
"example": {
"ssn": "555-55-5555",
"first_name": "John",
"last_name": "Doe",
"birth_date": "1965-01-01",
"middle_name": "Theodore",
"gender": "M"
}
}
}
}
}
}
}

//Example Request Body - default
{
"ssn": "555-55-5555",
"first_name": "John",
"last_name": "Doe",
"birth_date": "1965-01-01",
"middle_name": "Theodore",
"gender": "M"
}

//Example Request Body - required fields only
{
"ssn": "555-55-5555",
"first_name": "John",
"last_name": "Doe",
"birth_date": "1965-01-01"
}
```

</details>
</br>

<details><summary>Sample JSON - schema.properties "example" fields</summary>

```json
"paths": {
"/status": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ssn",
"first_name",
"last_name",
"birth_date"
],
"properties": {
"ssn": {
"type": "string",
"example": "555-55-5555"
},
"first_name": {
"type": "string",
"example": "John"
},
"last_name": {
"type": "string",
"example": "Doe"
},
"birth_date": {
"type": "string",
"example": "1965-01-01"
},
"middle_name": {
"type": "string",
"example": "Theodore"
},
"gender": {
"type": "string",
"enum": [
"M",
"F"
],
"example": "M"
}
}
}
}
}
}
}
}
}

//Example Request Body
//Example Request Body - default
{
"ssn": "555-55-5555",
"first_name": "John",
"last_name": "Doe",
"birth_date": "1965-01-01",
"middle_name": "Theodore",
"gender": "M"
}

//Example Request Body - required fields only
{
"ssn": "555-55-5555",
"first_name": "John",
Expand All @@ -599,6 +703,7 @@ Properties that are not required will not be included, even if a property exampl
```

</details>
</br>

# Validation

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 28 additions & 13 deletions src/oas-parsing/operation-example/operation-example.factory.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,39 @@
/**
* The OperationExampleFactory class is responsible for building OperationExamples for all Operations that are part of the OAS under test.
* An Operation could potentially have both multiple ExampleGroups (query, path, or header parameters)
* and multiple ExampleRequestBodies (a request body with only required fields and a request body with required and optional fields).
* OperationExamples will be created for each permutation of ExampleGroups and ExampleRequestBodies
* (if an Operation has 2 ExampleGroups and 2 ExampleRequestBodies, then 4 OperationExamples will be created for that Operation).
*/

import ExampleGroup from '../example-group/example-group';
import OASOperation from '../operation/oas-operation';
import OperationExample from './operation-example';

export default class OperationExampleFactory {
public static buildFromOperations(
operations: OASOperation[],
): OperationExample[] {
const operationExamples: OperationExample[] = [];

for (const operation of operations) {
const exampleGroups = operation.exampleGroups;
return operations.flatMap((operation) =>
this.buildFromOperation(operation),
);
}

for (const exampleGroup of exampleGroups) {
operationExamples.push({
operation,
exampleGroup,
requestBody: operation.exampleRequestBody,
});
}
}
private static buildFromOperation(
operation: OASOperation,
): OperationExample[] {
return operation.exampleGroups.flatMap((exampleGroup) =>
this.buildFromOperationAndExampleGroup(operation, exampleGroup),
);
}

return operationExamples;
private static buildFromOperationAndExampleGroup(
operation: OASOperation,
exampleGroup: ExampleGroup,
): OperationExample[] {
return operation.exampleRequestBodies.map(
(exampleRequestBody) =>
new OperationExample(operation, exampleGroup, exampleRequestBody),
);
}
}
16 changes: 12 additions & 4 deletions src/oas-parsing/operation-example/operation-example.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
import { RequestBody } from 'swagger-client';
import { NO_REQUEST_BODY } from '../../utilities/constants';
import ExampleGroup from '../example-group/example-group';
import OASOperation from '../operation/oas-operation';
import ExampleRequestBody from '../request-body/example-request-body';

export default class OperationExample {
readonly operation: OASOperation;

readonly exampleGroup: ExampleGroup;

readonly requestBody: RequestBody;
readonly exampleRequestBody: ExampleRequestBody;

readonly name: string;

constructor(
operation: OASOperation,
exampleGroup: ExampleGroup,
requestBody: RequestBody,
exampleRequestBody: ExampleRequestBody,
) {
this.operation = operation;
this.exampleGroup = exampleGroup;
this.requestBody = requestBody;
this.exampleRequestBody = exampleRequestBody;

this.name =
exampleRequestBody.name === NO_REQUEST_BODY
? exampleGroup.name
: `${exampleGroup.name} - ${exampleRequestBody.name}`;
}
}
11 changes: 6 additions & 5 deletions src/oas-parsing/operation/oas-operation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { OperationObject, ParameterObject, RequestBody } from 'swagger-client';
import { OperationObject, ParameterObject } from 'swagger-client';
import {
RequestBodyObject,
ResponseObject,
Expand All @@ -8,6 +8,7 @@ import ExampleGroup, { ExampleGroupFactory } from '../example-group';
import OASSecurity from '../security';
import OASSecurityFactory from '../security/oas-security.factory';
import RequestBodyFactory from '../request-body/request-body.factory';
import ExampleRequestBody from '../request-body/example-request-body';

class OASOperation {
readonly operation: OperationObject;
Expand All @@ -22,7 +23,7 @@ class OASOperation {

private _exampleGroups: ExampleGroup[];

private _exampleRequestBody: RequestBody;
private _exampleRequestBodies: ExampleRequestBody[];

constructor(
operation: OperationObject,
Expand All @@ -33,7 +34,7 @@ class OASOperation {
this.parameters = operation.parameters;
this.requestBody = operation.requestBody;
this._exampleGroups = ExampleGroupFactory.buildFromOperation(this);
this._exampleRequestBody = RequestBodyFactory.buildFromOperation(this);
this._exampleRequestBodies = RequestBodyFactory.buildFromOperation(this);
this.security = OASSecurityFactory.getSecurities(
operation.security ?? securities,
);
Expand All @@ -43,8 +44,8 @@ class OASOperation {
return [...this._exampleGroups];
}

get exampleRequestBody(): RequestBody {
return { ...this._exampleRequestBody };
get exampleRequestBodies(): ExampleRequestBody[] {
return [...this._exampleRequestBodies];
}

get requiredParameterNames(): string[] {
Expand Down
13 changes: 13 additions & 0 deletions src/oas-parsing/request-body/example-request-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { RequestBody } from 'swagger-client';

class ExampleRequestBody {
readonly name: string;
readonly requestBody: RequestBody;

constructor(name: string, requestBody: RequestBody) {
this.name = name;
this.requestBody = requestBody;
}
}

export default ExampleRequestBody;
Loading

0 comments on commit 9b0313b

Please sign in to comment.