From 96d844e4fc84d79b4441bfd7347a3eb580071cce Mon Sep 17 00:00:00 2001 From: Steve Houel Date: Fri, 25 Nov 2022 16:24:33 +0100 Subject: [PATCH 1/3] Adding new MatchmakingRuleSet construct and related tests --- packages/@aws-cdk/aws-gamelift/README.md | 71 +++++- packages/@aws-cdk/aws-gamelift/lib/index.ts | 2 + .../lib/matchmaking-ruleset-body.ts | 113 +++++++++ .../aws-gamelift/lib/matchmaking-ruleset.ts | 228 ++++++++++++++++++ ...efaultTestDeployAssert0688841C.assets.json | 19 ++ ...aultTestDeployAssert0688841C.template.json | 36 +++ .../aws-gamelift-build.assets.json | 19 ++ .../aws-gamelift-build.template.json | 60 +++++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 123 ++++++++++ .../tree.json | 142 +++++++++++ .../test/integ.matchmaking-ruleset.ts | 29 +++ .../test/matchmaking-ruleset-body.test.ts | 56 +++++ .../test/matchmaking-ruleset.test.ts | 137 +++++++++++ .../aws-gamelift/test/my-ruleset/ruleset.json | 57 +++++ 16 files changed, 1103 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts create mode 100644 packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.template.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts create mode 100644 packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts create mode 100644 packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts create mode 100644 packages/@aws-cdk/aws-gamelift/test/my-ruleset/ruleset.json diff --git a/packages/@aws-cdk/aws-gamelift/README.md b/packages/@aws-cdk/aws-gamelift/README.md index 1c52b8d24f06f..8ad2c88917397 100644 --- a/packages/@aws-cdk/aws-gamelift/README.md +++ b/packages/@aws-cdk/aws-gamelift/README.md @@ -50,6 +50,73 @@ deliver inexpensive, resilient game hosting for your players This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. It allows you to define components for your matchmaking configuration or game server fleet management system. +## GameLift FlexMatch + +### Matchmaking RuleSet + +Every FlexMatch matchmaker must have a rule set. The rule set determines the +two key elements of a match: your game's team structure and size, and how to +group players together for the best possible match. + +For example, a rule set might describe a match like this: Create a match with +two teams of four to eight players each, one team is the cowboy and the other +team the aliens. A team can have novice and experienced players, but the +average skill of the two teams must be within 10 points of each other. If no +match is made after 30 seconds, gradually relax the skill requirements. + +```ts +new gamelift.MatchmakingRuleSet(this, 'RuleSet', { + matchmakingRuleSetName: 'my-test-ruleset', + content: gamelift.RuleSetBody.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')), +}); +``` + +### FlexMatch Monitoring + +You can monitor GameLift FlexMatch activity for matchmaking configurations and +matchmaking rules using Amazon CloudWatch. These statistics are used to provide +a historical perspective on how your Gamelift FlexMatch solution is performing. + +#### FlexMatch Metrics + +GameLift FlexMatch sends metrics to CloudWatch so that you can collect and +analyze the activity of your matchmaking solution, including match acceptance +workflow, ticket consumtion. + +You can then use CloudWatch alarms to alert you, for example, when matches has +been rejected (potential matches that were rejected by at least one player +since the last report) exceed a certain thresold which could means that you may +have an issue in your matchmaking rules. + +CDK provides methods for accessing GameLift FlexMatch metrics with default configuration, +such as `metricRuleEvaluationsPassed`, or `metricRuleEvaluationsFailed` (see +[`IMatchmakingRuleSet`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-gamelift.IMatchmakingRuleSet.html) +for a full list). CDK also provides a generic `metric` method that can be used +to produce metric configurations for any metric provided by GameLift FlexMatch; +the configurations are pre-populated with the correct dimensions for the +matchmaking configuration. + +```ts +declare const matchmakingRuleSet: gamelift.MatchmakingRuleSet; +// Alarm that triggers when the per-second average of not placed matches exceed 10% +const ruleEvaluationRatio = new cloudwatch.MathExpression({ + expression: '1 - (ruleEvaluationsPassed / ruleEvaluationsFailed)', + usingMetrics: { + ruleEvaluationsPassed: matchmakingRuleSet.metricRuleEvaluationsPassed({ statistic: cloudwatch.Statistic.SUM }), + ruleEvaluationsFailed: matchmakingRuleSet.metric('ruleEvaluationsFailed'), + }, +}); +new cloudwatch.Alarm(this, 'Alarm', { + metric: ruleEvaluationRatio, + threshold: 0.1, + evaluationPeriods: 3, +}); +``` + +See: [Monitoring Using CloudWatch Metrics](https://docs.aws.amazon.com/gamelift/latest/developerguide/monitoring-cloudwatch.html) +in the *Amazon GameLift Developer Guide*. + + ## GameLift Hosting ### Uploading builds and scripts to GameLift @@ -320,7 +387,7 @@ fleet.grant(role, 'gamelift:ListFleets'); GameLift is integrated with CloudWatch, so you can monitor the performance of your game servers via logs and metrics. -#### Metrics +#### Fleet Metrics GameLift Fleet sends metrics to CloudWatch so that you can collect and analyze the activity of your Fleet, including game and player sessions and server @@ -493,7 +560,7 @@ new gamelift.GameServerGroup(this, 'GameServerGroup', { }); ``` -### Monitoring +### FleetIQ Monitoring GameLift FleetIQ sends metrics to CloudWatch so that you can collect and analyze the activity of your Game server fleet, including the number of diff --git a/packages/@aws-cdk/aws-gamelift/lib/index.ts b/packages/@aws-cdk/aws-gamelift/lib/index.ts index a6159851e5d3e..d57ec04eec9c6 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/index.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/index.ts @@ -5,6 +5,8 @@ export * from './game-server-group'; export * from './ingress-rule'; export * from './fleet-base'; export * from './build-fleet'; +export * from './matchmaking-ruleset'; +export * from './matchmaking-ruleset-body'; // AWS::GameLift CloudFormation Resources: export * from './gamelift.generated'; diff --git a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts new file mode 100644 index 0000000000000..b0d034e8e9987 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts @@ -0,0 +1,113 @@ +import * as fs from 'fs'; +import { Construct } from 'constructs'; + +/** + * The rule set determines the two key elements of a match: your game's team structure and size, and how to group players together for the best possible match. + * + * For example, a rule set might describe a match like this: + * - Create a match with two teams of five players each, one team is the defenders and the other team the invaders. + * - A team can have novice and experienced players, but the average skill of the two teams must be within 10 points of each other. + * - If no match is made after 30 seconds, gradually relax the skill requirements. + */ +export abstract class RuleSetBody { + + /** + * Matchmaking ruleSet body from a file + * @returns `JsonFileRuleSetBody` with inline code. + * @param path The path to the ruleSet body file + */ + public static fromJsonFile(path: string): RuleSetBody { + return new JsonFileRuleSetBody(path); + } + + /** + * Inline body for Matchmaking ruleSet + * @returns `InlineRuleSetBody` with inline code. + * @param body The actual ruleSet body (maximum 65535 characters) + */ + public static fromInline(body: string): RuleSetBody { + return new InlineRuleSetBody(body); + } + + /** + * Called when the matchmaking ruleSet is initialized to allow this object to bind + * to the stack and add resources. + * + * @param scope The binding scope. + */ + public abstract bind(scope: Construct): RuleSetBodyConfig; +} + +/** + * Result of binding `RuleSetBody` into a `MatchmakingRuleSet`. + */ +export interface RuleSetBodyConfig { + /** + * Inline ruleSet body. + */ + readonly ruleSetBody: string; +} + +/** + * Matchmaking ruleSet body from an inline string. + */ +export class InlineRuleSetBody extends RuleSetBody { + + /** + * @param path The ruleSet body. + */ + constructor(private body: string) { + super(); + + if (body.length === 0) { + throw new Error('Matchmaking ruleSet body cannot be empty'); + } + + if (body.length > 65535) { + throw new Error(`Matchmaking ruleSet body cannot exceed 65535 characters, actual ${body.length}`); + } + } + + public bind(_scope: Construct): RuleSetBodyConfig { + return { + ruleSetBody: this.body, + }; + } +} + +/** + * Matchmaking ruleSet body from aJSON File. + */ +export class JsonFileRuleSetBody extends RuleSetBody { + /** + * Json file body content + */ + private content: string; + + /** + * @param path The path to the ruleSert body file. + */ + constructor(private path: string) { + super(); + if (!fs.existsSync(path)) { + throw new Error(`Matchmaking ruleSet path does not exist, please verify it, actual ${this.path}`); + } + + if (!fs.lstatSync(path).isFile()) { + throw new Error(`Matchmaking ruleSet path is not link to a single file, please verify your path, actual ${this.path}`); + } + const file = fs.readFileSync(path); + + if (file.toString().length > 65535) { + throw new Error(`Matchmaking ruleSet body cannot exceed 65535 characters, actual ${file.toString().length}`); + } + + this.content = file.toString(); + } + + public bind(_scope: Construct): RuleSetBodyConfig { + return { + ruleSetBody: this.content, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts new file mode 100644 index 0000000000000..4fbd748ada969 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts @@ -0,0 +1,228 @@ +import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnMatchmakingRuleSet } from './gamelift.generated'; +import { RuleSetBody } from './matchmaking-ruleset-body'; + +/** + * Represents a Gamelift matchmaking ruleset + */ +export interface IMatchmakingRuleSet extends cdk.IResource { + /** + * The unique name of the ruleSet. + * + * @attribute + */ + readonly matchmakingRuleSetName: string; + + /** + * The ARN of the ruleSet. + * + * @attribute + */ + readonly matchmakingRuleSetArn: string; + + /** + * Return the given named metric for this matchmaking ruleSet. + */ + metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + /** + * Rule evaluations during the matchmaking process that passed since the last report. + * + * This metric is limited to the top 50 rules. + */ + metricRuleEvaluationsPassed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + + + /** + * Rule evaluations during matchmaking that failed since the last report. + * + * This metric is limited to the top 50 rules. + */ + metricRuleEvaluationsFailed(props?: cloudwatch.MetricOptions): cloudwatch.Metric; +} + +/** + * Properties for a new matchmaking ruleSet + */ +export interface MatchmakingRuleSetProps { + /** + * A unique identifier for the matchmaking rule set. + * A matchmaking configuration identifies the rule set it uses by this name value. + * + * Note: the rule set name is different from the optional name field in the rule set body + */ + readonly matchmakingRuleSetName: string; + + /** + * A collection of matchmaking rules. + */ + readonly content: RuleSetBody; +} + +/** + * A full specification of a matchmaking ruleSet that can be used to import it fluently into the CDK application. + */ +export interface MatchmakingRuleSetAttributes { + /** + * The ARN of the matchmaking ruleSet + * + * At least one of `matchmakingRuleSetArn` and `matchmakingRuleSetName` must be provided. + * + * @default derived from `matchmakingRuleSetName`. + */ + readonly matchmakingRuleSetArn?: string; + + /** + * The unique name of the matchmaking ruleSet + * + * At least one of `ruleSetName` and `matchmakingRuleSetArn` must be provided. + * + * @default derived from `matchmakingRuleSetArn`. + */ + readonly matchmakingRuleSetName?: string; + +} + +/** + * Base class for new and imported GameLift matchmaking ruleSet. + */ +export abstract class MatchmakingRuleSetBase extends cdk.Resource implements IMatchmakingRuleSet { + + /** + * The unique name of the ruleSet. + */ + public abstract readonly matchmakingRuleSetName: string; + /** + * The ARN of the ruleSet. + */ + public abstract readonly matchmakingRuleSetArn: string; + + public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return new cloudwatch.Metric({ + namespace: 'AWS/GameLift', + metricName: metricName, + dimensionsMap: { + FleetId: this.matchmakingRuleSetName, + }, + ...props, + }).attachTo(this); + } + + public metricRuleEvaluationsPassed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('RuleEvaluationsPassed', props); + } + public metricRuleEvaluationsFailed(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + return this.metric('RuleEvaluationsFailed', props); + } +} + +/** + * Creates a new rule set for FlexMatch matchmaking. + * + * The rule set determines the two key elements of a match: your game's team structure and size, and how to group players together for the best possible match. + * + * For example, a rule set might describe a match like this: + * - Create a match with two teams of five players each, one team is the defenders and the other team the invaders. + * - A team can have novice and experienced players, but the average skill of the two teams must be within 10 points of each other. + * - If no match is made after 30 seconds, gradually relax the skill requirements. + * + * Rule sets must be defined in the same Region as the matchmaking configuration they are used with. + * + * @see https://docs.aws.amazon.com/gamelift/latest/flexmatchguide/match-rulesets.html + * + * @resource AWS::GameLift::MatchmakingRuleSet + */ +export class MatchmakingRuleSet extends MatchmakingRuleSetBase { + + /** + * Import a ruleSet into CDK using its name + */ + static fromMatchmakingRuleSetName(scope: Construct, id: string, matchmakingRuleSetName: string): IMatchmakingRuleSet { + return this.fromMatchmakingRuleSetAttributes(scope, id, { matchmakingRuleSetName }); + } + + /** + * Import a ruleSet into CDK using its ARN + */ + static fromMatchmakingRuleSetArn(scope: Construct, id: string, matchmakingRuleSetArn: string): IMatchmakingRuleSet { + return this.fromMatchmakingRuleSetAttributes(scope, id, { matchmakingRuleSetArn }); + } + + /** + * Import an existing matchmaking ruleSet from its attributes. + */ + static fromMatchmakingRuleSetAttributes(scope: Construct, id: string, attrs: MatchmakingRuleSetAttributes): IMatchmakingRuleSet { + if (!attrs.matchmakingRuleSetName && !attrs.matchmakingRuleSetArn) { + throw new Error('Either matchmakingRuleSetName or matchmakingRuleSetArn must be provided in MatchmakingRuleSetAttributes'); + } + const matchmakingRuleSetName = attrs.matchmakingRuleSetName ?? + cdk.Stack.of(scope).splitArn(attrs.matchmakingRuleSetArn!, cdk.ArnFormat.SLASH_RESOURCE_NAME).resourceName; + + if (!matchmakingRuleSetName) { + throw new Error(`No matchmaking ruleSet identifier found in ARN: '${attrs.matchmakingRuleSetArn}'`); + } + + const matchmakingRuleSetArn = attrs.matchmakingRuleSetArn ?? cdk.Stack.of(scope).formatArn({ + service: 'gamelift', + resource: 'matchmakingruleset', + resourceName: attrs.matchmakingRuleSetName, + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + }); + class Import extends MatchmakingRuleSetBase { + public readonly matchmakingRuleSetName = matchmakingRuleSetName!; + public readonly matchmakingRuleSetArn = matchmakingRuleSetArn; + + constructor(s: Construct, i: string) { + super(s, i, { + environmentFromArn: matchmakingRuleSetArn, + }); + } + } + return new Import(scope, id); + } + + /** + * The unique name of the ruleSet. + */ + public readonly matchmakingRuleSetName: string; + + /** + * The ARN of the ruleSet. + */ + public readonly matchmakingRuleSetArn: string; + + constructor(scope: Construct, id: string, props: MatchmakingRuleSetProps) { + super(scope, id, { + physicalName: props.matchmakingRuleSetName, + }); + + if (!cdk.Token.isUnresolved(props.matchmakingRuleSetName)) { + if (props.matchmakingRuleSetName.length > 128) { + throw new Error(`RuleSet name can not be longer than 128 characters but has ${props.matchmakingRuleSetName.length} characters.`); + } + + if (!/^[a-zA-Z0-9-\.]+$/.test(props.matchmakingRuleSetName)) { + throw new Error(`RuleSet name ${props.matchmakingRuleSetName} can contain only letters, numbers, hyphens, back slash or dot with no spaces.`); + } + } + + const content = props.content.bind(this); + + const resource = new CfnMatchmakingRuleSet(this, 'Resource', { + name: props.matchmakingRuleSetName, + ruleSetBody: content.ruleSetBody, + }); + + this.matchmakingRuleSetName = this.getResourceNameAttribute(resource.ref); + this.matchmakingRuleSetArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'gamelift', + resource: 'matchmakingruleset', + resourceName: this.physicalName, + arnFormat: cdk.ArnFormat.SLASH_RESOURCE_NAME, + }); + } + + +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json new file mode 100644 index 0000000000000..c960bddbecb0d --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "BuildDefaultTestDeployAssert0688841C.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.template.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json new file mode 100644 index 0000000000000..8c7c2bd1f2cad --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "ab2dd0fb59ab0924d671fe62cecdf095d46a08706a1f60fbe2a9343b27898130": { + "source": { + "path": "aws-gamelift-build.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "ab2dd0fb59ab0924d671fe62cecdf095d46a08706a1f60fbe2a9343b27898130.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json new file mode 100644 index 0000000000000..f7bda953848c8 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json @@ -0,0 +1,60 @@ +{ + "Resources": { + "MatchmakingRuleSet360A3710": { + "Type": "AWS::GameLift::MatchmakingRuleSet", + "Properties": { + "Name": "my-test-ruleset", + "RuleSetBody": "{\n \"name\": \"three_team_game\",\n \"ruleLanguageVersion\": \"1.0\",\n \"playerAttributes\": [{\n \"name\": \"skill\",\n \"type\": \"number\",\n \"default\": 10\n },{\n \"name\": \"character\",\n \"type\": \"string_list\",\n \"default\": [ \"peasant\" ]\n }],\n \"teams\": [{\n \"name\": \"trio\",\n \"minPlayers\": 3,\n \"maxPlayers\": 5,\n \"quantity\": 3\n }],\n \"rules\": [{\n \"name\": \"FairTeamSkill\",\n \"description\": \"The average skill of players in each team is within 10 points from the average skill of players in the match\",\n \"type\": \"distance\",\n \"measurements\": [ \"avg(teams[*].players.attributes[skill])\" ],\n \"referenceValue\": \"avg(flatten(teams[*].players.attributes[skill]))\",\n \"maxDistance\": 10\n }, {\n \"name\": \"CloseTeamSizes\",\n \"description\": \"Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5\",\n \"type\": \"distance\",\n \"measurements\": [ \"max(count(teams[*].players))\"],\n \"referenceValue\": \"min(count(teams[*].players))\",\n \"maxDistance\": 1\n }, {\n \"name\": \"OverallMedicLimit\",\n \"description\": \"Don't allow more than 5 medics in the game\",\n \"type\": \"collection\",\n \"measurements\": [ \"flatten(teams[*].players.attributes[character])\"],\n \"operation\": \"contains\",\n \"referenceValue\": \"medic\",\n \"maxCount\": 5\n }, {\n \"name\": \"FastConnection\",\n \"description\": \"Prefer matches with fast player connections first\",\n \"type\": \"latency\",\n \"maxLatency\": 50\n }],\n \"expansions\": [{\n \"target\": \"rules[FastConnection].maxLatency\",\n \"steps\": [{\n \"waitTimeSeconds\": 10,\n \"value\": 100\n }, {\n \"waitTimeSeconds\": 20,\n \"value\": 150\n }]\n }]\n}" + } + } + }, + "Outputs": { + "MatchmakingRuleSetArn": { + "Value": { + "Fn::GetAtt": [ + "MatchmakingRuleSet360A3710", + "Arn" + ] + } + }, + "MatchmakingRuleSetName": { + "Value": { + "Ref": "MatchmakingRuleSet360A3710" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json new file mode 100644 index 0000000000000..7ab31f50206b7 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "21.0.0", + "testCases": { + "Build/DefaultTest": { + "stacks": [ + "aws-gamelift-build" + ], + "assertionStack": "Build/DefaultTest/DeployAssert", + "assertionStackName": "BuildDefaultTestDeployAssert0688841C" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json new file mode 100644 index 0000000000000..76b17d864c3df --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json @@ -0,0 +1,123 @@ +{ + "version": "21.0.0", + "artifacts": { + "aws-gamelift-build.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-gamelift-build.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-gamelift-build": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-gamelift-build.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ab2dd0fb59ab0924d671fe62cecdf095d46a08706a1f60fbe2a9343b27898130.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-gamelift-build.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-gamelift-build.assets" + ], + "metadata": { + "/aws-gamelift-build/MatchmakingRuleSet/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MatchmakingRuleSet360A3710" + } + ], + "/aws-gamelift-build/MatchmakingRuleSetArn": [ + { + "type": "aws:cdk:logicalId", + "data": "MatchmakingRuleSetArn" + } + ], + "/aws-gamelift-build/MatchmakingRuleSetName": [ + { + "type": "aws:cdk:logicalId", + "data": "MatchmakingRuleSetName" + } + ], + "/aws-gamelift-build/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-gamelift-build/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-gamelift-build" + }, + "BuildDefaultTestDeployAssert0688841C.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "BuildDefaultTestDeployAssert0688841C.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "BuildDefaultTestDeployAssert0688841C": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "BuildDefaultTestDeployAssert0688841C.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "BuildDefaultTestDeployAssert0688841C.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "BuildDefaultTestDeployAssert0688841C.assets" + ], + "metadata": { + "/Build/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/Build/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "Build/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json new file mode 100644 index 0000000000000..4d30fdd76f2fb --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json @@ -0,0 +1,142 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-gamelift-build": { + "id": "aws-gamelift-build", + "path": "aws-gamelift-build", + "children": { + "MatchmakingRuleSet": { + "id": "MatchmakingRuleSet", + "path": "aws-gamelift-build/MatchmakingRuleSet", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-gamelift-build/MatchmakingRuleSet/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::GameLift::MatchmakingRuleSet", + "aws:cdk:cloudformation:props": { + "name": "my-test-ruleset", + "ruleSetBody": "{\n \"name\": \"three_team_game\",\n \"ruleLanguageVersion\": \"1.0\",\n \"playerAttributes\": [{\n \"name\": \"skill\",\n \"type\": \"number\",\n \"default\": 10\n },{\n \"name\": \"character\",\n \"type\": \"string_list\",\n \"default\": [ \"peasant\" ]\n }],\n \"teams\": [{\n \"name\": \"trio\",\n \"minPlayers\": 3,\n \"maxPlayers\": 5,\n \"quantity\": 3\n }],\n \"rules\": [{\n \"name\": \"FairTeamSkill\",\n \"description\": \"The average skill of players in each team is within 10 points from the average skill of players in the match\",\n \"type\": \"distance\",\n \"measurements\": [ \"avg(teams[*].players.attributes[skill])\" ],\n \"referenceValue\": \"avg(flatten(teams[*].players.attributes[skill]))\",\n \"maxDistance\": 10\n }, {\n \"name\": \"CloseTeamSizes\",\n \"description\": \"Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5\",\n \"type\": \"distance\",\n \"measurements\": [ \"max(count(teams[*].players))\"],\n \"referenceValue\": \"min(count(teams[*].players))\",\n \"maxDistance\": 1\n }, {\n \"name\": \"OverallMedicLimit\",\n \"description\": \"Don't allow more than 5 medics in the game\",\n \"type\": \"collection\",\n \"measurements\": [ \"flatten(teams[*].players.attributes[character])\"],\n \"operation\": \"contains\",\n \"referenceValue\": \"medic\",\n \"maxCount\": 5\n }, {\n \"name\": \"FastConnection\",\n \"description\": \"Prefer matches with fast player connections first\",\n \"type\": \"latency\",\n \"maxLatency\": 50\n }],\n \"expansions\": [{\n \"target\": \"rules[FastConnection].maxLatency\",\n \"steps\": [{\n \"waitTimeSeconds\": 10,\n \"value\": 100\n }, {\n \"waitTimeSeconds\": 20,\n \"value\": 150\n }]\n }]\n}" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.CfnMatchmakingRuleSet", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.MatchmakingRuleSet", + "version": "0.0.0" + } + }, + "MatchmakingRuleSetArn": { + "id": "MatchmakingRuleSetArn", + "path": "aws-gamelift-build/MatchmakingRuleSetArn", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "MatchmakingRuleSetName": { + "id": "MatchmakingRuleSetName", + "path": "aws-gamelift-build/MatchmakingRuleSetName", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnOutput", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-gamelift-build/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-gamelift-build/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "Build": { + "id": "Build", + "path": "Build", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "Build/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "Build/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "Build/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "Build/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "Build/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.161" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts new file mode 100644 index 0000000000000..169d22fc1b5b2 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts @@ -0,0 +1,29 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import { CfnOutput } from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import { Construct } from 'constructs'; +import * as gamelift from '../lib'; + +class TestStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const ruleSet = new gamelift.MatchmakingRuleSet(this, 'MatchmakingRuleSet', { + matchmakingRuleSetName: 'my-test-ruleset', + content: gamelift.RuleSetBody.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')), + }); + + new CfnOutput(this, 'MatchmakingRuleSetArn', { value: ruleSet.matchmakingRuleSetArn }); + new CfnOutput(this, 'MatchmakingRuleSetName', { value: ruleSet.matchmakingRuleSetName }); + } +} + +// Beginning of the test suite +const app = new cdk.App(); +const stack = new TestStack(app, 'aws-gamelift-build'); +new IntegTest(app, 'Build', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts new file mode 100644 index 0000000000000..5a2935c3860e3 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts @@ -0,0 +1,56 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as gamelift from '../lib'; + +describe('MatchmakingRuleSetBody', () => { + let stack: cdk.Stack; + + beforeEach(() => { + stack = new cdk.Stack(); + }); + + describe('gamelift.MatchmakingRuleSetBody.fromInline', () => { + test('new RuleSetBody from Inline content', () => { + const ruleSet = gamelift.RuleSetBody.fromInline('{}'); + const content = ruleSet.bind(stack); + expect(content.ruleSetBody).toEqual('{}'); + }); + + test('fails if empty content', () => { + expect(() => gamelift.RuleSetBody.fromInline('')) + .toThrow(/Matchmaking ruleSet body cannot be empty/); + }); + + test('fails if content too large', () => { + let incorrectContent = ''; + for (let i = 0; i < 65536; i++) { + incorrectContent += 'A'; + } + + expect(() => gamelift.RuleSetBody.fromInline(incorrectContent)) + .toThrow(/Matchmaking ruleSet body cannot exceed 65535 characters, actual 65536/); + }); + }); + + describe('gamelift.MatchmakingRuleSetBody.fromJsonFile', () => { + test('new RuleSetBody from Json file', () => { + const ruleSet = gamelift.RuleSetBody.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')); + const content = ruleSet.bind(stack); + const result = fs.readFileSync(path.join(__dirname, 'my-ruleset/ruleset.json')); + expect(content.ruleSetBody).toEqual(result.toString()); + }); + + test('fails if file not exist', () => { + const content = path.join(__dirname, 'my-ruleset/file-not-exist.json'); + expect(() => gamelift.RuleSetBody.fromJsonFile(content)) + .toThrow(`Matchmaking ruleSet path does not exist, please verify it, actual ${content}`); + }); + + test('fails if file is a directory', () => { + const content = path.join(__dirname, 'my-ruleset'); + expect(() => gamelift.RuleSetBody.fromJsonFile(content)) + .toThrow(`Matchmaking ruleSet path is not link to a single file, please verify your path, actual ${content}`); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts new file mode 100644 index 0000000000000..d2ff6b2e2174f --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts @@ -0,0 +1,137 @@ +import { Template } from '@aws-cdk/assertions'; +import * as cdk from '@aws-cdk/core'; +import * as gamelift from '../lib'; + +describe('MatchmakingRuleSet', () => { + + describe('new', () => { + let stack: cdk.Stack; + const ruleSetBody = '{}'; + + beforeEach(() => { + stack = new cdk.Stack(); + }); + + test('default new ruleSet', () => { + new gamelift.MatchmakingRuleSet(stack, 'MyMatchmakingRuleSet', { + matchmakingRuleSetName: 'test-ruleSet', + content: gamelift.RuleSetBody.fromInline(ruleSetBody), + }); + + Template.fromStack(stack).hasResource('AWS::GameLift::MatchmakingRuleSet', { + Properties: + { + Name: 'test-ruleSet', + RuleSetBody: ruleSetBody, + }, + }); + }); + + test('with an incorrect name - too long', () => { + let incorrectName = ''; + for (let i = 0; i < 129; i++) { + incorrectName += 'A'; + } + + expect(() => new gamelift.MatchmakingRuleSet(stack, 'MyMatchmakingRuleSet', { + matchmakingRuleSetName: incorrectName, + content: gamelift.RuleSetBody.fromInline(ruleSetBody), + })).toThrow(/RuleSet name can not be longer than 128 characters but has 129 characters./); + }); + + test('with an incorrect name - bad format', () => { + let incorrectName = 'test with space'; + + expect(() => new gamelift.MatchmakingRuleSet(stack, 'MyMatchmakingRuleSet', { + matchmakingRuleSetName: incorrectName, + content: gamelift.RuleSetBody.fromInline(ruleSetBody), + })).toThrow(/RuleSet name test with space can contain only letters, numbers, hyphens, back slash or dot with no spaces./); + }); + }); + + describe('test import methods', () => { + test('MatchmakingRuleSet.fromMatchmakingRuleSetArn', () => { + // GIVEN + const stack2 = new cdk.Stack(); + + // WHEN + const imported = gamelift.MatchmakingRuleSet.fromMatchmakingRuleSetArn(stack2, 'Imported', 'arn:aws:gamelift:us-east-1:123456789012:matchmakingruleset/sample-ruleSet-name'); + + // THEN + expect(imported.matchmakingRuleSetArn).toEqual('arn:aws:gamelift:us-east-1:123456789012:matchmakingruleset/sample-ruleSet-name'); + expect(imported.matchmakingRuleSetName).toEqual('sample-ruleSet-name'); + }); + + test('MatchmakingRuleSet.fromMatchmakingRuleSetName', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const imported = gamelift.MatchmakingRuleSet.fromMatchmakingRuleSetName(stack, 'Imported', 'sample-ruleSet-name'); + + // THEN + expect(stack.resolve(imported.matchmakingRuleSetArn)).toStrictEqual({ + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':gamelift:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':matchmakingruleset/sample-ruleSet-name', + ]], + }); + expect(stack.resolve(imported.matchmakingRuleSetName)).toStrictEqual('sample-ruleSet-name'); + }); + }); + + describe('MatchmakingRuleSet.fromMatchmakingRuleSetAttributes()', () => { + let stack: cdk.Stack; + const matchmakingRuleSetName = 'ruleSet-test-identifier'; + const matchmakingRuleSetArn = `arn:aws:gamelift:ruleSet-region:123456789012:matchmakingruleset/${matchmakingRuleSetName}`; + + beforeEach(() => { + const app = new cdk.App(); + stack = new cdk.Stack(app, 'Base', { + env: { account: '111111111111', region: 'stack-region' }, + }); + }); + + describe('', () => { + test('with required attrs only', () => { + const importedFleet = gamelift.MatchmakingRuleSet.fromMatchmakingRuleSetAttributes(stack, 'ImportedMatchmakingRuleSet', { matchmakingRuleSetArn }); + + expect(importedFleet.matchmakingRuleSetName).toEqual(matchmakingRuleSetName); + expect(importedFleet.matchmakingRuleSetArn).toEqual(matchmakingRuleSetArn); + expect(importedFleet.env.account).toEqual('123456789012'); + expect(importedFleet.env.region).toEqual('ruleSet-region'); + }); + + test('with missing attrs', () => { + expect(() => gamelift.MatchmakingRuleSet.fromMatchmakingRuleSetAttributes(stack, 'ImportedMatchmakingRuleSet', { })) + .toThrow(/Either matchmakingRuleSetName or matchmakingRuleSetArn must be provided in MatchmakingRuleSetAttributes/); + }); + + test('with invalid ARN', () => { + expect(() => gamelift.MatchmakingRuleSet.fromMatchmakingRuleSetAttributes(stack, 'ImportedMatchmakingRuleSet', { matchmakingRuleSetArn: 'arn:aws:gamelift:ruleSet-region:123456789012:matchmakingruleset' })) + .toThrow(/No matchmaking ruleSet identifier found in ARN: 'arn:aws:gamelift:ruleSet-region:123456789012:matchmakingruleset'/); + }); + }); + + describe('for an ruleSet in a different account and region', () => { + let ruleSet: gamelift.IMatchmakingRuleSet; + + beforeEach(() => { + ruleSet = gamelift.MatchmakingRuleSet.fromMatchmakingRuleSetAttributes(stack, 'ImportedMatchmakingRuleSet', { matchmakingRuleSetArn }); + }); + + test("the ruleSet's region is taken from the ARN", () => { + expect(ruleSet.env.region).toBe('ruleSet-region'); + }); + + test("the ruleSet's account is taken from the ARN", () => { + expect(ruleSet.env.account).toBe('123456789012'); + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/my-ruleset/ruleset.json b/packages/@aws-cdk/aws-gamelift/test/my-ruleset/ruleset.json new file mode 100644 index 0000000000000..a490527207ba1 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/my-ruleset/ruleset.json @@ -0,0 +1,57 @@ +{ + "name": "three_team_game", + "ruleLanguageVersion": "1.0", + "playerAttributes": [{ + "name": "skill", + "type": "number", + "default": 10 + },{ + "name": "character", + "type": "string_list", + "default": [ "peasant" ] + }], + "teams": [{ + "name": "trio", + "minPlayers": 3, + "maxPlayers": 5, + "quantity": 3 + }], + "rules": [{ + "name": "FairTeamSkill", + "description": "The average skill of players in each team is within 10 points from the average skill of players in the match", + "type": "distance", + "measurements": [ "avg(teams[*].players.attributes[skill])" ], + "referenceValue": "avg(flatten(teams[*].players.attributes[skill]))", + "maxDistance": 10 + }, { + "name": "CloseTeamSizes", + "description": "Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5", + "type": "distance", + "measurements": [ "max(count(teams[*].players))"], + "referenceValue": "min(count(teams[*].players))", + "maxDistance": 1 + }, { + "name": "OverallMedicLimit", + "description": "Don't allow more than 5 medics in the game", + "type": "collection", + "measurements": [ "flatten(teams[*].players.attributes[character])"], + "operation": "contains", + "referenceValue": "medic", + "maxCount": 5 + }, { + "name": "FastConnection", + "description": "Prefer matches with fast player connections first", + "type": "latency", + "maxLatency": 50 + }], + "expansions": [{ + "target": "rules[FastConnection].maxLatency", + "steps": [{ + "waitTimeSeconds": 10, + "value": 100 + }, { + "waitTimeSeconds": 20, + "value": 150 + }] + }] +} \ No newline at end of file From 38bdf8905fc98b45bd49af6d4f08be7c30a4abc6 Mon Sep 17 00:00:00 2001 From: Steve Houel Date: Mon, 5 Dec 2022 15:25:38 +0100 Subject: [PATCH 2/3] Adding MatchmakingRuleSet JSON format check --- .../lib/matchmaking-ruleset-body.ts | 129 ++++++++---------- .../aws-gamelift/lib/matchmaking-ruleset.ts | 6 +- ...efaultTestDeployAssert0688841C.assets.json | 2 +- .../aws-gamelift-build.assets.json | 6 +- .../aws-gamelift-build.template.json | 2 +- .../cdk.out | 2 +- .../integ.json | 2 +- .../manifest.json | 4 +- .../tree.json | 6 +- .../test/integ.matchmaking-ruleset.ts | 2 +- .../test/matchmaking-ruleset-body.test.ts | 28 ++-- .../test/matchmaking-ruleset.test.ts | 8 +- 12 files changed, 94 insertions(+), 103 deletions(-) diff --git a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts index b0d034e8e9987..a0b10324f5352 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts @@ -2,112 +2,103 @@ import * as fs from 'fs'; import { Construct } from 'constructs'; /** - * The rule set determines the two key elements of a match: your game's team structure and size, and how to group players together for the best possible match. - * - * For example, a rule set might describe a match like this: - * - Create a match with two teams of five players each, one team is the defenders and the other team the invaders. - * - A team can have novice and experienced players, but the average skill of the two teams must be within 10 points of each other. - * - If no match is made after 30 seconds, gradually relax the skill requirements. + * Interface to represent Matchmaking RuleSet schema */ -export abstract class RuleSetBody { +export interface IRuleSetBody {} +/** + * Interface to represent output result of a RuleSetContent binding + */ +export interface RuleSetBodyConfig { /** - * Matchmaking ruleSet body from a file - * @returns `JsonFileRuleSetBody` with inline code. - * @param path The path to the ruleSet body file - */ - public static fromJsonFile(path: string): RuleSetBody { - return new JsonFileRuleSetBody(path); - } + * Inline ruleSet body. + */ + readonly ruleSetBody: string; +} + +/** + * Interface to represent a Matchmaking RuleSet content + */ +export interface IRuleSetContent { /** - * Inline body for Matchmaking ruleSet - * @returns `InlineRuleSetBody` with inline code. - * @param body The actual ruleSet body (maximum 65535 characters) + * RuleSet body content + * + * @attribute */ - public static fromInline(body: string): RuleSetBody { - return new InlineRuleSetBody(body); - } + readonly content: IRuleSetBody; /** * Called when the matchmaking ruleSet is initialized to allow this object to bind * to the stack and add resources. * - * @param scope The binding scope. + * @param _scope The binding scope. */ - public abstract bind(scope: Construct): RuleSetBodyConfig; + bind(_scope: Construct): RuleSetBodyConfig; } /** - * Result of binding `RuleSetBody` into a `MatchmakingRuleSet`. - */ -export interface RuleSetBodyConfig { - /** - * Inline ruleSet body. - */ - readonly ruleSetBody: string; -} - -/** - * Matchmaking ruleSet body from an inline string. + * The rule set determines the two key elements of a match: your game's team structure and size, and how to group players together for the best possible match. + * + * For example, a rule set might describe a match like this: + * - Create a match with two teams of five players each, one team is the defenders and the other team the invaders. + * - A team can have novice and experienced players, but the average skill of the two teams must be within 10 points of each other. + * - If no match is made after 30 seconds, gradually relax the skill requirements. */ -export class InlineRuleSetBody extends RuleSetBody { +export class RuleSetContent implements IRuleSetContent { /** - * @param path The ruleSet body. + * Matchmaking ruleSet body from a file + * @returns `RuleSetContentBase` based on JSON file content. + * @param path The path to the ruleSet body file */ - constructor(private body: string) { - super(); - - if (body.length === 0) { - throw new Error('Matchmaking ruleSet body cannot be empty'); + public static fromJsonFile(path: string): IRuleSetContent { + if (!fs.existsSync(path)) { + throw new Error(`RuleSet path does not exist, please verify it, actual ${path}`); } - if (body.length > 65535) { - throw new Error(`Matchmaking ruleSet body cannot exceed 65535 characters, actual ${body.length}`); + if (!fs.lstatSync(path).isFile()) { + throw new Error(`RuleSet path is not link to a single file, please verify your path, actual ${path}`); } - } + const file = fs.readFileSync(path); - public bind(_scope: Construct): RuleSetBodyConfig { - return { - ruleSetBody: this.body, - }; + return this.fromInline(file.toString()); } -} -/** - * Matchmaking ruleSet body from aJSON File. - */ -export class JsonFileRuleSetBody extends RuleSetBody { /** - * Json file body content + * Inline body for Matchmaking ruleSet + * @returns `RuleSetContentBase` with inline code. + * @param body The actual ruleSet body (maximum 65535 characters) */ - private content: string; + public static fromInline(body: string): IRuleSetContent { + return new RuleSetContent(body); + } /** - * @param path The path to the ruleSert body file. + * RuleSet body content */ - constructor(private path: string) { - super(); - if (!fs.existsSync(path)) { - throw new Error(`Matchmaking ruleSet path does not exist, please verify it, actual ${this.path}`); - } + public readonly content: IRuleSetBody; - if (!fs.lstatSync(path).isFile()) { - throw new Error(`Matchmaking ruleSet path is not link to a single file, please verify your path, actual ${this.path}`); + constructor(body?: string) { + if (body && body.length > 65535) { + throw new Error(`RuleSet body cannot exceed 65535 characters, actual ${body.length}`); } - const file = fs.readFileSync(path); - - if (file.toString().length > 65535) { - throw new Error(`Matchmaking ruleSet body cannot exceed 65535 characters, actual ${file.toString().length}`); + try { + this.content = body && JSON.parse(body) || {}; + } catch (err) { + throw new Error('RuleSet body has an invalid Json format'); } - - this.content = file.toString(); } + /** + * Called when the matchmaking ruleSet is initialized to allow this object to bind + * to the stack and add resources. + * + * @param _scope The binding scope. + */ public bind(_scope: Construct): RuleSetBodyConfig { return { - ruleSetBody: this.content, + ruleSetBody: JSON.stringify(this.content), }; } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts index 4fbd748ada969..eaa500f0f3979 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset.ts @@ -2,7 +2,8 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnMatchmakingRuleSet } from './gamelift.generated'; -import { RuleSetBody } from './matchmaking-ruleset-body'; +import { RuleSetContent } from './matchmaking-ruleset-body'; + /** * Represents a Gamelift matchmaking ruleset @@ -58,7 +59,7 @@ export interface MatchmakingRuleSetProps { /** * A collection of matchmaking rules. */ - readonly content: RuleSetBody; + readonly content: RuleSetContent; } /** @@ -207,7 +208,6 @@ export class MatchmakingRuleSet extends MatchmakingRuleSetBase { throw new Error(`RuleSet name ${props.matchmakingRuleSetName} can contain only letters, numbers, hyphens, back slash or dot with no spaces.`); } } - const content = props.content.bind(this); const resource = new CfnMatchmakingRuleSet(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json index c960bddbecb0d..b35a218c4d89e 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/BuildDefaultTestDeployAssert0688841C.assets.json @@ -1,5 +1,5 @@ { - "version": "21.0.0", + "version": "22.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json index 8c7c2bd1f2cad..de6ee49a07f2e 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.assets.json @@ -1,7 +1,7 @@ { - "version": "21.0.0", + "version": "22.0.0", "files": { - "ab2dd0fb59ab0924d671fe62cecdf095d46a08706a1f60fbe2a9343b27898130": { + "3136917a7c2150678ed63057dcd3ea4e3218f8bca520725e637878a51ebc06ee": { "source": { "path": "aws-gamelift-build.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "ab2dd0fb59ab0924d671fe62cecdf095d46a08706a1f60fbe2a9343b27898130.json", + "objectKey": "3136917a7c2150678ed63057dcd3ea4e3218f8bca520725e637878a51ebc06ee.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json index f7bda953848c8..f34da5610324e 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/aws-gamelift-build.template.json @@ -4,7 +4,7 @@ "Type": "AWS::GameLift::MatchmakingRuleSet", "Properties": { "Name": "my-test-ruleset", - "RuleSetBody": "{\n \"name\": \"three_team_game\",\n \"ruleLanguageVersion\": \"1.0\",\n \"playerAttributes\": [{\n \"name\": \"skill\",\n \"type\": \"number\",\n \"default\": 10\n },{\n \"name\": \"character\",\n \"type\": \"string_list\",\n \"default\": [ \"peasant\" ]\n }],\n \"teams\": [{\n \"name\": \"trio\",\n \"minPlayers\": 3,\n \"maxPlayers\": 5,\n \"quantity\": 3\n }],\n \"rules\": [{\n \"name\": \"FairTeamSkill\",\n \"description\": \"The average skill of players in each team is within 10 points from the average skill of players in the match\",\n \"type\": \"distance\",\n \"measurements\": [ \"avg(teams[*].players.attributes[skill])\" ],\n \"referenceValue\": \"avg(flatten(teams[*].players.attributes[skill]))\",\n \"maxDistance\": 10\n }, {\n \"name\": \"CloseTeamSizes\",\n \"description\": \"Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5\",\n \"type\": \"distance\",\n \"measurements\": [ \"max(count(teams[*].players))\"],\n \"referenceValue\": \"min(count(teams[*].players))\",\n \"maxDistance\": 1\n }, {\n \"name\": \"OverallMedicLimit\",\n \"description\": \"Don't allow more than 5 medics in the game\",\n \"type\": \"collection\",\n \"measurements\": [ \"flatten(teams[*].players.attributes[character])\"],\n \"operation\": \"contains\",\n \"referenceValue\": \"medic\",\n \"maxCount\": 5\n }, {\n \"name\": \"FastConnection\",\n \"description\": \"Prefer matches with fast player connections first\",\n \"type\": \"latency\",\n \"maxLatency\": 50\n }],\n \"expansions\": [{\n \"target\": \"rules[FastConnection].maxLatency\",\n \"steps\": [{\n \"waitTimeSeconds\": 10,\n \"value\": 100\n }, {\n \"waitTimeSeconds\": 20,\n \"value\": 150\n }]\n }]\n}" + "RuleSetBody": "{\"name\":\"three_team_game\",\"ruleLanguageVersion\":\"1.0\",\"playerAttributes\":[{\"name\":\"skill\",\"type\":\"number\",\"default\":10},{\"name\":\"character\",\"type\":\"string_list\",\"default\":[\"peasant\"]}],\"teams\":[{\"name\":\"trio\",\"minPlayers\":3,\"maxPlayers\":5,\"quantity\":3}],\"rules\":[{\"name\":\"FairTeamSkill\",\"description\":\"The average skill of players in each team is within 10 points from the average skill of players in the match\",\"type\":\"distance\",\"measurements\":[\"avg(teams[*].players.attributes[skill])\"],\"referenceValue\":\"avg(flatten(teams[*].players.attributes[skill]))\",\"maxDistance\":10},{\"name\":\"CloseTeamSizes\",\"description\":\"Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5\",\"type\":\"distance\",\"measurements\":[\"max(count(teams[*].players))\"],\"referenceValue\":\"min(count(teams[*].players))\",\"maxDistance\":1},{\"name\":\"OverallMedicLimit\",\"description\":\"Don't allow more than 5 medics in the game\",\"type\":\"collection\",\"measurements\":[\"flatten(teams[*].players.attributes[character])\"],\"operation\":\"contains\",\"referenceValue\":\"medic\",\"maxCount\":5},{\"name\":\"FastConnection\",\"description\":\"Prefer matches with fast player connections first\",\"type\":\"latency\",\"maxLatency\":50}],\"expansions\":[{\"target\":\"rules[FastConnection].maxLatency\",\"steps\":[{\"waitTimeSeconds\":10,\"value\":100},{\"waitTimeSeconds\":20,\"value\":150}]}]}" } } }, diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out index 8ecc185e9dbee..145739f539580 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"21.0.0"} \ No newline at end of file +{"version":"22.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json index 7ab31f50206b7..afc2a36ad8185 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "21.0.0", + "version": "22.0.0", "testCases": { "Build/DefaultTest": { "stacks": [ diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json index 76b17d864c3df..77ef071d85ea7 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "21.0.0", + "version": "22.0.0", "artifacts": { "aws-gamelift-build.assets": { "type": "cdk:asset-manifest", @@ -17,7 +17,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ab2dd0fb59ab0924d671fe62cecdf095d46a08706a1f60fbe2a9343b27898130.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/3136917a7c2150678ed63057dcd3ea4e3218f8bca520725e637878a51ebc06ee.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json index 4d30fdd76f2fb..7d1746af43f3c 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.js.snapshot/tree.json @@ -19,7 +19,7 @@ "aws:cdk:cloudformation:type": "AWS::GameLift::MatchmakingRuleSet", "aws:cdk:cloudformation:props": { "name": "my-test-ruleset", - "ruleSetBody": "{\n \"name\": \"three_team_game\",\n \"ruleLanguageVersion\": \"1.0\",\n \"playerAttributes\": [{\n \"name\": \"skill\",\n \"type\": \"number\",\n \"default\": 10\n },{\n \"name\": \"character\",\n \"type\": \"string_list\",\n \"default\": [ \"peasant\" ]\n }],\n \"teams\": [{\n \"name\": \"trio\",\n \"minPlayers\": 3,\n \"maxPlayers\": 5,\n \"quantity\": 3\n }],\n \"rules\": [{\n \"name\": \"FairTeamSkill\",\n \"description\": \"The average skill of players in each team is within 10 points from the average skill of players in the match\",\n \"type\": \"distance\",\n \"measurements\": [ \"avg(teams[*].players.attributes[skill])\" ],\n \"referenceValue\": \"avg(flatten(teams[*].players.attributes[skill]))\",\n \"maxDistance\": 10\n }, {\n \"name\": \"CloseTeamSizes\",\n \"description\": \"Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5\",\n \"type\": \"distance\",\n \"measurements\": [ \"max(count(teams[*].players))\"],\n \"referenceValue\": \"min(count(teams[*].players))\",\n \"maxDistance\": 1\n }, {\n \"name\": \"OverallMedicLimit\",\n \"description\": \"Don't allow more than 5 medics in the game\",\n \"type\": \"collection\",\n \"measurements\": [ \"flatten(teams[*].players.attributes[character])\"],\n \"operation\": \"contains\",\n \"referenceValue\": \"medic\",\n \"maxCount\": 5\n }, {\n \"name\": \"FastConnection\",\n \"description\": \"Prefer matches with fast player connections first\",\n \"type\": \"latency\",\n \"maxLatency\": 50\n }],\n \"expansions\": [{\n \"target\": \"rules[FastConnection].maxLatency\",\n \"steps\": [{\n \"waitTimeSeconds\": 10,\n \"value\": 100\n }, {\n \"waitTimeSeconds\": 20,\n \"value\": 150\n }]\n }]\n}" + "ruleSetBody": "{\"name\":\"three_team_game\",\"ruleLanguageVersion\":\"1.0\",\"playerAttributes\":[{\"name\":\"skill\",\"type\":\"number\",\"default\":10},{\"name\":\"character\",\"type\":\"string_list\",\"default\":[\"peasant\"]}],\"teams\":[{\"name\":\"trio\",\"minPlayers\":3,\"maxPlayers\":5,\"quantity\":3}],\"rules\":[{\"name\":\"FairTeamSkill\",\"description\":\"The average skill of players in each team is within 10 points from the average skill of players in the match\",\"type\":\"distance\",\"measurements\":[\"avg(teams[*].players.attributes[skill])\"],\"referenceValue\":\"avg(flatten(teams[*].players.attributes[skill]))\",\"maxDistance\":10},{\"name\":\"CloseTeamSizes\",\"description\":\"Only launch a game when the team sizes are within 1 of each other. e.g. 3 v 3 v 4 is okay, but not 3 v 5 v 5\",\"type\":\"distance\",\"measurements\":[\"max(count(teams[*].players))\"],\"referenceValue\":\"min(count(teams[*].players))\",\"maxDistance\":1},{\"name\":\"OverallMedicLimit\",\"description\":\"Don't allow more than 5 medics in the game\",\"type\":\"collection\",\"measurements\":[\"flatten(teams[*].players.attributes[character])\"],\"operation\":\"contains\",\"referenceValue\":\"medic\",\"maxCount\":5},{\"name\":\"FastConnection\",\"description\":\"Prefer matches with fast player connections first\",\"type\":\"latency\",\"maxLatency\":50}],\"expansions\":[{\"target\":\"rules[FastConnection].maxLatency\",\"steps\":[{\"waitTimeSeconds\":10,\"value\":100},{\"waitTimeSeconds\":20,\"value\":150}]}]}" } }, "constructInfo": { @@ -84,7 +84,7 @@ "path": "Build/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.161" + "version": "10.1.168" } }, "DeployAssert": { @@ -130,7 +130,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.161" + "version": "10.1.168" } } }, diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts index 169d22fc1b5b2..93273d6da1195 100644 --- a/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts +++ b/packages/@aws-cdk/aws-gamelift/test/integ.matchmaking-ruleset.ts @@ -11,7 +11,7 @@ class TestStack extends cdk.Stack { const ruleSet = new gamelift.MatchmakingRuleSet(this, 'MatchmakingRuleSet', { matchmakingRuleSetName: 'my-test-ruleset', - content: gamelift.RuleSetBody.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')), + content: gamelift.RuleSetContent.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')), }); new CfnOutput(this, 'MatchmakingRuleSetArn', { value: ruleSet.matchmakingRuleSetArn }); diff --git a/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts index 5a2935c3860e3..ac725b0418077 100644 --- a/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts +++ b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset-body.test.ts @@ -12,14 +12,14 @@ describe('MatchmakingRuleSetBody', () => { describe('gamelift.MatchmakingRuleSetBody.fromInline', () => { test('new RuleSetBody from Inline content', () => { - const ruleSet = gamelift.RuleSetBody.fromInline('{}'); + const ruleSet = gamelift.RuleSetContent.fromInline('{}'); const content = ruleSet.bind(stack); expect(content.ruleSetBody).toEqual('{}'); }); - test('fails if empty content', () => { - expect(() => gamelift.RuleSetBody.fromInline('')) - .toThrow(/Matchmaking ruleSet body cannot be empty/); + test('fails if invlaid JSON format', () => { + expect(() => gamelift.RuleSetContent.fromInline('{ name }')) + .toThrow(/RuleSet body has an invalid Json format/); }); test('fails if content too large', () => { @@ -28,29 +28,29 @@ describe('MatchmakingRuleSetBody', () => { incorrectContent += 'A'; } - expect(() => gamelift.RuleSetBody.fromInline(incorrectContent)) - .toThrow(/Matchmaking ruleSet body cannot exceed 65535 characters, actual 65536/); + expect(() => gamelift.RuleSetContent.fromInline(JSON.stringify({ name: incorrectContent }))) + .toThrow(/RuleSet body cannot exceed 65535 characters, actual 65547/); }); }); describe('gamelift.MatchmakingRuleSetBody.fromJsonFile', () => { test('new RuleSetBody from Json file', () => { - const ruleSet = gamelift.RuleSetBody.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')); + const ruleSet = gamelift.RuleSetContent.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')); const content = ruleSet.bind(stack); - const result = fs.readFileSync(path.join(__dirname, 'my-ruleset/ruleset.json')); - expect(content.ruleSetBody).toEqual(result.toString()); + const result = JSON.parse(fs.readFileSync(path.join(__dirname, 'my-ruleset/ruleset.json')).toString()); + expect(content.ruleSetBody).toEqual(JSON.stringify(result)); }); test('fails if file not exist', () => { const content = path.join(__dirname, 'my-ruleset/file-not-exist.json'); - expect(() => gamelift.RuleSetBody.fromJsonFile(content)) - .toThrow(`Matchmaking ruleSet path does not exist, please verify it, actual ${content}`); + expect(() => gamelift.RuleSetContent.fromJsonFile(content)) + .toThrow(`RuleSet path does not exist, please verify it, actual ${content}`); }); test('fails if file is a directory', () => { - const content = path.join(__dirname, 'my-ruleset'); - expect(() => gamelift.RuleSetBody.fromJsonFile(content)) - .toThrow(`Matchmaking ruleSet path is not link to a single file, please verify your path, actual ${content}`); + const contentPath = path.join(__dirname, 'my-ruleset'); + expect(() => gamelift.RuleSetContent.fromJsonFile(contentPath)) + .toThrow(`RuleSet path is not link to a single file, please verify your path, actual ${contentPath}`); }); }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts index d2ff6b2e2174f..f04acc0467403 100644 --- a/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts +++ b/packages/@aws-cdk/aws-gamelift/test/matchmaking-ruleset.test.ts @@ -6,7 +6,7 @@ describe('MatchmakingRuleSet', () => { describe('new', () => { let stack: cdk.Stack; - const ruleSetBody = '{}'; + const ruleSetBody = JSON.stringify('{}'); beforeEach(() => { stack = new cdk.Stack(); @@ -15,7 +15,7 @@ describe('MatchmakingRuleSet', () => { test('default new ruleSet', () => { new gamelift.MatchmakingRuleSet(stack, 'MyMatchmakingRuleSet', { matchmakingRuleSetName: 'test-ruleSet', - content: gamelift.RuleSetBody.fromInline(ruleSetBody), + content: gamelift.RuleSetContent.fromInline(ruleSetBody), }); Template.fromStack(stack).hasResource('AWS::GameLift::MatchmakingRuleSet', { @@ -35,7 +35,7 @@ describe('MatchmakingRuleSet', () => { expect(() => new gamelift.MatchmakingRuleSet(stack, 'MyMatchmakingRuleSet', { matchmakingRuleSetName: incorrectName, - content: gamelift.RuleSetBody.fromInline(ruleSetBody), + content: gamelift.RuleSetContent.fromInline(ruleSetBody), })).toThrow(/RuleSet name can not be longer than 128 characters but has 129 characters./); }); @@ -44,7 +44,7 @@ describe('MatchmakingRuleSet', () => { expect(() => new gamelift.MatchmakingRuleSet(stack, 'MyMatchmakingRuleSet', { matchmakingRuleSetName: incorrectName, - content: gamelift.RuleSetBody.fromInline(ruleSetBody), + content: gamelift.RuleSetContent.fromInline(ruleSetBody), })).toThrow(/RuleSet name test with space can contain only letters, numbers, hyphens, back slash or dot with no spaces./); }); }); From 5ce27831cded860d586634cd754836ce30ed1a90 Mon Sep 17 00:00:00 2001 From: Steve Houel Date: Mon, 5 Dec 2022 16:33:23 +0100 Subject: [PATCH 3/3] Update matchmaking ruleset class property interface format --- packages/@aws-cdk/aws-gamelift/README.md | 2 +- .../lib/matchmaking-ruleset-body.ts | 37 +++++++++++++------ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-gamelift/README.md b/packages/@aws-cdk/aws-gamelift/README.md index 15134a70df885..5631d73d2722d 100644 --- a/packages/@aws-cdk/aws-gamelift/README.md +++ b/packages/@aws-cdk/aws-gamelift/README.md @@ -67,7 +67,7 @@ match is made after 30 seconds, gradually relax the skill requirements. ```ts new gamelift.MatchmakingRuleSet(this, 'RuleSet', { matchmakingRuleSetName: 'my-test-ruleset', - content: gamelift.RuleSetBody.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')), + content: gamelift.RuleSetContent.fromJsonFile(path.join(__dirname, 'my-ruleset/ruleset.json')), }); ``` diff --git a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts index a0b10324f5352..4c7a086cec18e 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/matchmaking-ruleset-body.ts @@ -37,6 +37,19 @@ export interface IRuleSetContent { bind(_scope: Construct): RuleSetBodyConfig; } +/** + * Properties for a new matchmaking ruleSet content + */ +export interface RuleSetContentProps { + + /** + * RuleSet body content + * + * @default use a default empty RuleSet body + */ + readonly content?: IRuleSetBody; +} + /** * The rule set determines the two key elements of a match: your game's team structure and size, and how to group players together for the best possible match. * @@ -67,29 +80,31 @@ export class RuleSetContent implements IRuleSetContent { /** * Inline body for Matchmaking ruleSet - * @returns `RuleSetContentBase` with inline code. + * @returns `RuleSetContent` with inline code. * @param body The actual ruleSet body (maximum 65535 characters) */ public static fromInline(body: string): IRuleSetContent { - return new RuleSetContent(body); - } - - /** - * RuleSet body content - */ - public readonly content: IRuleSetBody; - - constructor(body?: string) { if (body && body.length > 65535) { throw new Error(`RuleSet body cannot exceed 65535 characters, actual ${body.length}`); } try { - this.content = body && JSON.parse(body) || {}; + return new RuleSetContent({ + content: JSON.parse(body), + }); } catch (err) { throw new Error('RuleSet body has an invalid Json format'); } } + /** + * RuleSet body content + */ + public readonly content: IRuleSetBody; + + constructor(props: RuleSetContentProps) { + this.content = props.content || {}; + } + /** * Called when the matchmaking ruleSet is initialized to allow this object to bind * to the stack and add resources.