Skip to content

Commit

Permalink
Make minimal additions to start adding an API gateway Model type, wit…
Browse files Browse the repository at this point in the history
…h a single test.

Add a test for adding a model with valid JSON schema. Document some test cases.

Add support for optional model name/description properties.

Add test for consumption of a model in a MethodResponse.

Add test case for importing an existing model, and using it.

Clean up testing todo list.

Add test case for exporting a model.

Update formatting from 4 to 2 spaces.

Create IModelConstruct to expose IConstruct properties on concrete implementations of IModel.

Fix tests from rebase to updated master.

Add method to RestApi to direclty add a model.

Tweak some documentation and add an integration test for an API with defined models.

Expose property on model that can be used to generate a canonical reference for another external model to consume.

Correct typo in README, and add some comments into code sample.

Update comments and make minor syntactic changes.

Split ModelProps into two interfaces to handle the constructor vs addModel method cases.

Clean up IModel awslint exclusions. Rename IModel -> IModelRef, and IModelConstruct -> IModel.

Add JSON schema type safety to the schema property of Model.

Fix test errors after merge with 'master'
  • Loading branch information
john-shaskin committed Apr 1, 2019
1 parent fb9fef2 commit b1bdb3d
Show file tree
Hide file tree
Showing 12 changed files with 84,425 additions and 30 deletions.
66 changes: 65 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,70 @@ 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: 'http://json-schema.org/draft-04/schema#',
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: 'http://json-schema.org/draft-04/schema#',
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: [{
statusCode: '200',
responseModels: {
'application/json': beerModelJson,
'application/xml': beerModelXml,
}
}]
});
```

### Deployments

By default, the `RestApi` construct will automatically create an API Gateway
Expand Down Expand Up @@ -213,7 +277,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
72 changes: 72 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,72 @@
/**
* 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'];
}
44 changes: 22 additions & 22 deletions packages/@aws-cdk/aws-apigateway/lib/methodresponse.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { IModel } from './model';
import { IModelRef } from './model';

export interface MethodResponse {

/**
* The method response's status code, which you map to an IntegrationResponse.
* Required.
*/
readonly statusCode: string;
/**
* 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 };
/**
* 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 };
/**
* 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 };
}
163 changes: 156 additions & 7 deletions packages/@aws-cdk/aws-apigateway/lib/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,60 @@
export interface IModel {
readonly modelId: string;
import cdk = require('@aws-cdk/cdk');
import { CfnModel } from './apigateway.generated';
import { JsonSchema, JsonSchemaMapper } from './json-schema';
import { IRestApi } from './restapi';

export interface ModelImportProps {
readonly modelId: string;
}

export interface ModelBaseProps {
/**
* The content type for the model.
* Required.
*/
readonly contentType: string;

/**
* The schema to use to transform data to one or more output formats.
* Specify null ({}) if you don't want to specify a schema.
* Required.
* @see http://json-schema.org/
*/
readonly schema: JsonSchema;

/**
* A description that identifies this model.
* Optional
* @default None
*/
readonly description?: string;
}

export interface ModelProps extends ModelBaseProps {
/**
* The ID of a REST API with which to associate this model.
* Required.
*/
readonly restApi: IRestApi;

/**
* A name for the model. If you don't specify a name, AWS CloudFormation
* generates a unique physical ID and uses that ID for the model name.
* Optional.
* @default None
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-name.html
*/
readonly name?: string;
}

/**
* Base model interface. Here for common support providing both:
* 1. A reference to the default Empty and Error models.
* 2. Extended to support creating/importing models as constructs.
*/
export interface IModelRef {
readonly modelId: string;
}

/**
Expand All @@ -19,8 +74,8 @@ export interface IModel {
*
* @see https://docs.amazonaws.cn/en_us/apigateway/latest/developerguide/models-mappings.html#models-mappings-models
*/
export class EmptyModel implements IModel {
public readonly modelId = 'Empty';
export class EmptyModel implements IModelRef {
public readonly modelId = 'Empty';
}

/**
Expand All @@ -39,8 +94,102 @@ export class EmptyModel implements IModel {
* }
* }
*/
export class ErrorModel implements IModel {
public readonly modelId = 'Error';
export class ErrorModel implements IModelRef {
public readonly modelId = 'Error';
}

/**
* For defining instances of a model
*/
export interface IModel extends cdk.IConstruct, IModelRef {
/**
* Exports a Model resource from this stack.
* @returns Model props that can be imported to another stack.
*/
export(): ModelImportProps;
}

/**
* Defines an REST API model
*/
export class Model extends cdk.Construct implements IModel {

/**
* Returns a reference to the default Empty model.
*/
public static readonly Empty: IModelRef = new EmptyModel();

/**
* Returns a reference to the default Error model.
*/
public static readonly Error: IModelRef = new ErrorModel();

/**
* Import an existing model from another stack
* @param scope
* @param id
* @param props
*/
public static import(scope: cdk.Construct, id: string, props: ModelImportProps): IModel {
return new ImportedModel(scope, id, props);
}

/**
* The identifier for this model.
*/
public readonly modelId: string;

/**
* The REST API that this model belongs to.
*/
public readonly restApi: IRestApi;

constructor(scope: cdk.Construct, id: string, props: ModelProps) {
super(scope, id);

const model = new CfnModel(this, 'Model', {
restApiId: props.restApi.restApiId,
contentType: props.contentType,
schema: JsonSchemaMapper.toCfnJsonSchema(props.schema),
name: props.name,
description: props.description,
});

this.modelId = model.ref;
this.restApi = props.restApi;
}

/**
* Exports a Model resource from this stack.
* @returns Model props that can be imported to another stack.
*/
public export(): ModelImportProps {
return {
modelId: new cdk.CfnOutput(this, 'ModelId', { value: this.modelId }).makeImportValue().toString(),
};
}

/**
* Generates the formatted reference to this model, so that another model can reference it, in its schema
*/
public get referenceForSchema(): string {
return `https://apigateway.${cdk.Aws.urlSuffix}/restapis/${this.restApi.restApiId}/models/${this.modelId}`;
}
}

// TODO: Implement Model, enabling management of custom models.
/**
* Defines a reference to a model created outside of the current CloudFormation stack.
*/
class ImportedModel extends cdk.Construct implements IModel {
public readonly modelId: string;

constructor(scope: cdk.Construct, id: string, private readonly props: ModelImportProps) {
super(scope, id);

this.modelId = props.modelId;
}

public export() {
return this.props;
}
}
Loading

0 comments on commit b1bdb3d

Please sign in to comment.