Skip to content

Commit a430601

Browse files
committed
change resource state import from CodeAction to CompletionItem with tabstop placeholders
1 parent b3d588c commit a430601

12 files changed

+734
-154
lines changed

src/resourceState/ResourceStateImporter.ts

Lines changed: 185 additions & 30 deletions
Large diffs are not rendered by default.

src/resourceState/ResourceStateTypes.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { CodeAction, CodeActionParams } from 'vscode-languageserver';
1+
import { CompletionItem, TextDocumentIdentifier } from 'vscode-languageserver';
22
import { RequestType } from 'vscode-languageserver-protocol';
33
import { ResourceStackManagementResult } from './StackManagementInfoProvider';
44

@@ -27,12 +27,14 @@ export enum ResourceStatePurpose {
2727
CLONE = 'Clone',
2828
}
2929

30-
export interface ResourceStateParams extends CodeActionParams {
30+
export interface ResourceStateParams {
31+
textDocument: TextDocumentIdentifier;
3132
resourceSelections?: ResourceSelection[];
3233
purpose: ResourceStatePurpose;
3334
}
3435

35-
export interface ResourceStateResult extends CodeAction {
36+
export interface ResourceStateResult {
37+
completionItem?: CompletionItem;
3638
successfulImports: Record<ResourceType, ResourceIdentifier[]>;
3739
failedImports: Record<ResourceType, ResourceIdentifier[]>;
3840
warning?: string;

src/schema/transformers/AddWriteOnlyRequiredPropertiesTransformer.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import type { ResourceSchema } from '../ResourceSchema';
2+
import { PlaceholderConstants } from './PlaceholderConstants';
23
import type { ResourceTemplateTransformer } from './ResourceTemplateTransformer';
34

45
/**
5-
* Transformer that adds tabstop placeholders for required write-only properties.
6+
* Transformer that adds placeholder constants for required write-only properties.
67
* Only adds placeholders at the required property level, not for nested write-only children.
7-
* Replaces empty objects with tabstop placeholders using LSP snippet syntax.
8-
* Uses sequential tabstops (${1}, ${2}, etc.). The $0 final cursor position is handled by the client.
8+
* Uses placeholder constants that will be replaced with tab stops later.
99
*/
1010
export class AddWriteOnlyRequiredPropertiesTransformer implements ResourceTemplateTransformer {
11-
public transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema): void {
11+
public transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema, logicalId?: string): void {
1212
const requiredProps = schema.required ?? [];
1313
const writeOnlyPaths = schema.writeOnlyProperties ?? [];
1414

15-
if (requiredProps.length === 0 || writeOnlyPaths.length === 0) {
15+
if (requiredProps.length === 0 || writeOnlyPaths.length === 0 || !logicalId) {
1616
return;
1717
}
1818

@@ -28,10 +28,12 @@ export class AddWriteOnlyRequiredPropertiesTransformer implements ResourceTempla
2828
}
2929
}
3030

31-
let tabstopIndex = 1;
3231
for (const prop of requiredWriteOnlyProps) {
3332
if (!(prop in resourceProperties) || this.isEmpty(resourceProperties[prop])) {
34-
resourceProperties[prop] = `\${${tabstopIndex++}:update required write only property}`;
33+
resourceProperties[prop] = PlaceholderConstants.createPlaceholder(
34+
PlaceholderConstants.WRITE_ONLY_REQUIRED,
35+
logicalId,
36+
);
3537
}
3638
}
3739
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Constants for placeholder text used during resource transformation.
3+
* These will be replaced with actual snippet tab stops at the end of processing.
4+
*/
5+
export const PlaceholderConstants = {
6+
/** Common prefix for all placeholders */
7+
PREFIX: '__PLACEHOLDER__',
8+
9+
/** Placeholder purposes */
10+
WRITE_ONLY_REQUIRED: 'WRITE_ONLY_REQUIRED',
11+
CLONE_INPUT_REQUIRED: 'CLONE_INPUT_REQUIRED',
12+
13+
/** Generate placeholder text with logical ID */
14+
createPlaceholder(purpose: string, logicalId: string): string {
15+
return `${this.PREFIX}${purpose}__${logicalId}`;
16+
},
17+
18+
/** Check if text contains any placeholder */
19+
hasPlaceholders(text: string): boolean {
20+
return text.includes(this.PREFIX);
21+
},
22+
} as const;
23+
24+
/**
25+
* Utility to replace placeholder constants with snippet tab stops.
26+
*/
27+
export const PlaceholderReplacer = {
28+
/**
29+
* Replace all placeholder constants with sequential snippet tab stops.
30+
* @param text The text containing placeholder constants
31+
* @returns Text with tab stops (${1:...}, ${2:...}, etc.)
32+
*/
33+
replaceWithTabStops(text: string): string {
34+
let tabStopCounter = 1;
35+
let result = text;
36+
37+
// Find all placeholders in the text
38+
const placeholderRegex = /__PLACEHOLDER__(\w+)__(\w+)/g;
39+
const placeholders: Array<{ match: string; purpose: string; logicalId: string }> = [];
40+
41+
let match;
42+
while ((match = placeholderRegex.exec(text)) !== null) {
43+
placeholders.push({
44+
match: match[0],
45+
purpose: match[1],
46+
logicalId: match[2],
47+
});
48+
}
49+
50+
// Replace each placeholder sequentially using local counter
51+
for (const placeholder of placeholders) {
52+
const purposeText =
53+
placeholder.purpose === PlaceholderConstants.WRITE_ONLY_REQUIRED
54+
? 'write only required property'
55+
: 'enter new identifier';
56+
57+
const tabStopText = `\${${tabStopCounter++}:${purposeText} for ${placeholder.logicalId}}`;
58+
result = result.replace(placeholder.match, tabStopText);
59+
}
60+
61+
return result;
62+
},
63+
64+
/**
65+
* Check if text contains any placeholder constants.
66+
*/
67+
hasPlaceholders(text: string): boolean {
68+
return PlaceholderConstants.hasPlaceholders(text);
69+
},
70+
} as const;

src/schema/transformers/ReplacePrimaryIdentifierTransformer.ts

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,67 @@
11
import { ResourceSchema } from '../ResourceSchema';
2+
import { PlaceholderConstants } from './PlaceholderConstants';
23
import { ResourceTemplateTransformer } from './ResourceTemplateTransformer';
34

45
export class ReplacePrimaryIdentifierTransformer implements ResourceTemplateTransformer {
5-
private static readonly CLONE_PLACEHOLDER = '<CLONE INPUT REQUIRED>';
6-
7-
transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema): void {
6+
transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema, logicalId?: string): void {
87
if (!schema.primaryIdentifier || schema.primaryIdentifier.length === 0) {
98
return;
109
}
1110

1211
for (const identifierPath of schema.primaryIdentifier) {
13-
this.replacePrimaryIdentifierProperty(resourceProperties, identifierPath);
12+
if (this.isPrimaryIdentifierRequired(identifierPath, schema)) {
13+
if (logicalId) {
14+
this.replacePrimaryIdentifierProperty(resourceProperties, identifierPath, logicalId);
15+
}
16+
} else {
17+
this.removePrimaryIdentifierProperty(resourceProperties, identifierPath);
18+
}
19+
}
20+
}
21+
22+
private isPrimaryIdentifierRequired(identifierPath: string, schema: ResourceSchema): boolean {
23+
if (!schema.required || schema.required.length === 0) {
24+
return false;
25+
}
26+
27+
const pathParts = identifierPath.split('/').filter((part) => part !== '' && part !== 'properties');
28+
if (pathParts.length === 0) {
29+
return false;
1430
}
31+
32+
const propertyName = pathParts[pathParts.length - 1];
33+
return schema.required.includes(propertyName);
34+
}
35+
36+
private removePrimaryIdentifierProperty(properties: Record<string, unknown>, propertyPath: string): void {
37+
const pathParts = propertyPath.split('/').filter((part) => part !== '' && part !== 'properties');
38+
39+
if (pathParts.length === 0) {
40+
return;
41+
}
42+
43+
let current = properties;
44+
45+
// Navigate to the parent of the target property
46+
for (let i = 0; i < pathParts.length - 1; i++) {
47+
const part = pathParts[i];
48+
if (current[part] && typeof current[part] === 'object') {
49+
current = current[part] as Record<string, unknown>;
50+
} else {
51+
return; // Path doesn't exist
52+
}
53+
}
54+
55+
// Remove the final property
56+
const finalProperty = pathParts[pathParts.length - 1];
57+
delete current[finalProperty];
1558
}
1659

17-
private replacePrimaryIdentifierProperty(properties: Record<string, unknown>, propertyPath: string): void {
60+
private replacePrimaryIdentifierProperty(
61+
properties: Record<string, unknown>,
62+
propertyPath: string,
63+
logicalId: string,
64+
): void {
1865
// Handle nested property paths like "/properties/BucketName"
1966
const pathParts = propertyPath.split('/').filter((part) => part !== '' && part !== 'properties');
2067

@@ -37,7 +84,10 @@ export class ReplacePrimaryIdentifierTransformer implements ResourceTemplateTran
3784
// Replace the final property with placeholder
3885
const finalProperty = pathParts[pathParts.length - 1];
3986
if (finalProperty in current) {
40-
current[finalProperty] = ReplacePrimaryIdentifierTransformer.CLONE_PLACEHOLDER;
87+
current[finalProperty] = PlaceholderConstants.createPlaceholder(
88+
PlaceholderConstants.CLONE_INPUT_REQUIRED,
89+
logicalId,
90+
);
4191
}
4292
}
4393
}

src/schema/transformers/ResourceTemplateTransformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import { ResourceSchema } from '../ResourceSchema';
44
* Interface for resource template transformers
55
*/
66
export interface ResourceTemplateTransformer {
7-
transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema): void;
7+
transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema, logicalId?: string): void;
88
}

tst/unit/resourceState/MockResourceState.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,40 @@ export const MockResourceStates = {
196196
Description: 'Database connection URL',
197197
}),
198198
} as ResourceState,
199+
200+
'AWS::Synthetics::Canary': {
201+
typeName: 'AWS::Synthetics::Canary',
202+
identifier: 'my-test-canary',
203+
createdTimestamp: baseTimestamp,
204+
properties: JSON.stringify({
205+
Name: 'my-test-canary',
206+
Code: {
207+
Handler: 'index.handler',
208+
Script: 'exports.handler = async () => {};',
209+
},
210+
ArtifactS3Location: 's3://my-bucket/canary-artifacts',
211+
ExecutionRoleArn: 'arn:aws:iam::123456789012:role/canary-role',
212+
Schedule: {
213+
Expression: 'rate(5 minutes)',
214+
},
215+
RuntimeVersion: 'syn-nodejs-puppeteer-3.9',
216+
}),
217+
} as ResourceState,
218+
219+
'AWS::SecurityLake::SubscriberNotification': {
220+
typeName: 'AWS::SecurityLake::SubscriberNotification',
221+
identifier: 'arn:aws:securitylake:us-east-1:123456789012:subscriber/test-subscriber',
222+
createdTimestamp: baseTimestamp,
223+
properties: JSON.stringify({
224+
SubscriberArn: 'arn:aws:securitylake:us-east-1:123456789012:subscriber/test-subscriber',
225+
NotificationConfiguration: {
226+
HttpsNotificationConfiguration: {
227+
TargetRoleArn: 'arn:aws:iam::123456789012:role/notification-role',
228+
Endpoint: 'https://example.com/webhook',
229+
},
230+
},
231+
}),
232+
} as ResourceState,
199233
};
200234

201235
export function createMockResourceState(resourceType: string): ResourceState {

0 commit comments

Comments
 (0)