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

feat: add support for defining/importing api gateway models #2102

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,71 @@ const proxy = resource.addProxy({
});
```

### Models

You may define models to provide structure to request payloads sent to a method,
and/or payloads returned to a client from a method. A model may be added to a
REST API as follows:

```ts
// Direct addition to REST API
const beerModel = api.addModel('Beer', {
contentType: 'application/json',
schema: {
$schema: JsonSchemaSchema.draft4,
title: 'Beer',
type: 'object',
properties: {
name: { type: 'string' },
style: { type: 'string' },
abv: { type: 'number' },
ibu: { type: 'number' },
},
},
description: 'Simple model for defining a beer.',
});

// Separate instantiation
const breweryModel = new apiGateway.Model(this, 'breweryModel', {
restApi: api,
name: 'Brewery',
description: 'Simple model for defining a brewery.',
contentType: 'application/json',
schema: {
$schema: JsonSchemaSchema.draft4,
title: 'Brewery',
type: 'object',
properties: {
name: { type: 'string' },
address: { type: 'string' },
// Reference another model
beers: { type: 'array', items: { $ref: beerModel.referenceForSchema } }
},
},
});
```

Given a model definition, you may either define a binding to request or response of a method via:

#### Request

RequestModel support not yet implemented.

#### Response

```ts
const method = api.beer.addMethod('GET', getBeerLambdaHandler, {
methodResponses: [
new MethodResponse({ statusCode: '200' })
.addResponseModel(beerModelJson)
.addResonseModel(beerModelXml),
new MethodResponse({ statusCode: '400' })
.addResponseModel(errorModelJson)
.addResonseModel(errorModelXml),
]
});
```

### Deployments

By default, the `RestApi` construct will automatically create an API Gateway
Expand Down Expand Up @@ -213,7 +278,7 @@ list of missing features.

### Roadmap

- [ ] Support defining REST API Models [#1695](https://github.com/awslabs/aws-cdk/issues/1695)
- Validate REST API model JSON schema

----

Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-apigateway/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export * from './lambda-api';
export * from './vpc-link';
export * from './methodresponse';
export * from './model';
export * from './json-schema';

// AWS::ApiGateway CloudFormation Resources:
export * from './apigateway.generated';
Expand Down
86 changes: 86 additions & 0 deletions packages/@aws-cdk/aws-apigateway/lib/json-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* Represents a JSON schema definition of the structure of a
* REST API model. Copied from npm module jsonschema.
*
* @see http://json-schema.org/
* @see https://github.com/tdegrunt/jsonschema
*/
export interface JsonSchema {
readonly id?: string;
// Exported as $schema - JSII linting does not like the $
readonly schema?: string;
// Exported as $ref - JSII linting does not like the $
readonly ref?: string;
readonly title?: string;
readonly description?: string;
readonly multipleOf?: number;
readonly maximum?: number;
readonly exclusiveMaximum?: boolean;
readonly minimum?: number;
readonly exclusiveMinimum?: boolean;
readonly maxLength?: number;
readonly minLength?: number;
readonly pattern?: string;
readonly additionalItems?: boolean | JsonSchema;
readonly items?: JsonSchema | JsonSchema[];
readonly maxItems?: number;
readonly minItems?: number;
readonly uniqueItems?: boolean;
readonly maxProperties?: number;
readonly minProperties?: number;
readonly required?: string[];
readonly additionalProperties?: boolean | JsonSchema;
readonly definitions?: {
[name: string]: JsonSchema;
};
readonly properties?: {
[name: string]: JsonSchema;
};
readonly patternProperties?: {
[name: string]: JsonSchema;
};
readonly dependencies?: {
[name: string]: JsonSchema | string[];
};
readonly 'enum'?: any[];
readonly type?: string | string[];
readonly format?: string;
readonly allOf?: JsonSchema[];
readonly anyOf?: JsonSchema[];
readonly oneOf?: JsonSchema[];
readonly not?: JsonSchema;
}

export class JsonSchemaMapper {
/**
* Transforms naming of some properties to prefix with a $, where needed
* according to the JSON schema spec
* @param jsonSchema The JsonSchema object to transform for CloudFormation output
*/
public static toCfnJsonSchema(jsonSchema: JsonSchema): any {
let cfnJsonSchema: string = JSON.stringify(jsonSchema);
JsonSchemaMapper.PropsWithPrefix.forEach(prop => {
const propKey = `"${prop}":`;
const propReplace = `"$${prop}":`;
cfnJsonSchema = cfnJsonSchema.replace(propKey, propReplace);
});

return JSON.parse(cfnJsonSchema);
}

private static readonly PropsWithPrefix = ['schema', 'ref'];
}

export class JsonSchemaSchema {
public static get draft4(): string {
return JsonSchemaSchema.schemaUrlTemplate('draft-04');
}

public static get draft7(): string {
return JsonSchemaSchema.schemaUrlTemplate('draft-07');
}

public static schemaUrlTemplate(draft: string): string {
return `http://json-schema.org/${draft}/schema#`;
}
}
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-apigateway/lib/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import cdk = require('@aws-cdk/cdk');
import { CfnMethod, CfnMethodProps } from './apigateway.generated';
import { ConnectionType, Integration } from './integration';
import { MockIntegration } from './integrations/mock';
import { MethodResponse } from './methodresponse';
import { IMethodResponse } from './methodresponse';
import { IRestApiResource } from './resource';
import { RestApi } from './restapi';
import { validateHttpMethod } from './util';
Expand Down Expand Up @@ -44,7 +44,7 @@ export interface MethodOptions {
* for the integration response to be correctly mapped to a response to the client.
* @see https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-method-settings-method-response.html
*/
readonly methodResponses?: MethodResponse[]
readonly methodResponses?: IMethodResponse[]

/**
* The request parameters that API Gateway accepts. Specify request parameters
Expand Down Expand Up @@ -209,7 +209,7 @@ export class Method extends cdk.Construct {
};
}

private renderMethodResponses(methodResponses: MethodResponse[] | undefined): CfnMethod.MethodResponseProperty[] | undefined {
private renderMethodResponses(methodResponses: IMethodResponse[] | undefined): CfnMethod.MethodResponseProperty[] | undefined {
if (!methodResponses) {
// Fall back to nothing
return undefined;
Expand Down
93 changes: 66 additions & 27 deletions packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,67 @@
import { IModel } from './model';

export interface MethodResponse {

/**
* The method response's status code, which you map to an IntegrationResponse.
* Required.
*/
readonly statusCode: string;

/**
* Response parameters that API Gateway sends to the client that called a method.
* Specify response parameters as key-value pairs (string-to-Boolean maps), with
* a destination as the key and a Boolean as the value. Specify the destination
* using the following pattern: method.response.header.name, where the name is a
* valid, unique header name. The Boolean specifies whether a parameter is required.
* @default None
*/
readonly responseParameters?: { [destination: string]: boolean };

/**
* The resources used for the response's content type. Specify response models as
* key-value pairs (string-to-string maps), with a content type as the key and a Model
* resource name as the value.
* @default None
*/
readonly responseModels?: { [contentType: string]: IModel };
import { IModelRef, Model } from './model';

export interface IMethodResponse {

/**
* The method response's status code, which you map to an IntegrationResponse.
* Required.
*/
readonly statusCode: string;

/**
* Response parameters that API Gateway sends to the client that called a method.
* Specify response parameters as key-value pairs (string-to-Boolean maps), with
* a destination as the key and a Boolean as the value. Specify the destination
* using the following pattern: method.response.header.name, where the name is a
* valid, unique header name. The Boolean specifies whether a parameter is required.
* @default None
*/
readonly responseParameters?: { [destination: string]: boolean };

/**
* The resources used for the response's content type. Specify response models as
* key-value pairs (string-to-string maps), with a content type as the key and a Model
* resource name as the value.
* @default None
*/
readonly responseModels?: { [contentType: string]: IModelRef };
}

export interface MethodResponseProps {
readonly statusCode: string;
readonly responseParameters?: { [destination: string]: boolean };
readonly responseModels?: { [contentType: string]: IModelRef };
}

export class MethodResponse implements IMethodResponse {
public readonly statusCode: string;
public readonly responseParameters?: { [destination: string]: boolean };
private responseModelsInt?: { [contentType: string]: IModelRef };

public get responseModels(): { [contentType: string]: IModelRef } | undefined {
return this.responseModelsInt;
}

constructor(props: MethodResponseProps) {
this.statusCode = props.statusCode;
this.responseParameters = props.responseParameters;
this.responseModelsInt = props.responseModels;
}

public addResponseModelForContentType(contentType: string, model: IModelRef) {
if (!this.responseModelsInt) {
this.responseModelsInt = {};
}

if (this.responseModelsInt[contentType]) {
throw new Error(`A model has already been registered for the content type ${contentType}`);
}
this.responseModelsInt[contentType] = model;

return this;
}

public addResponseModel(model: Model): MethodResponse {
return this.addResponseModelForContentType(model.contentType, model);
}
}
Loading