Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ResourceSchema } from '../ResourceSchema';
import type { ResourceTemplateTransformer } from './ResourceTemplateTransformer';

/**
* Transformer that adds tabstop placeholders for required write-only properties.
* Only adds placeholders at the required property level, not for nested write-only children.
* Replaces empty objects with tabstop placeholders using LSP snippet syntax.
* Uses sequential tabstops (${1}, ${2}, etc.). The $0 final cursor position is handled by the client.
*/
export class AddWriteOnlyRequiredPropertiesTransformer implements ResourceTemplateTransformer {
public transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema): void {
const requiredProps = schema.required ?? [];
const writeOnlyPaths = schema.writeOnlyProperties ?? [];

if (requiredProps.length === 0 || writeOnlyPaths.length === 0) {
return;
}

const requiredWriteOnlyProps = new Set<string>();

for (const path of writeOnlyPaths) {
const parts = this.parseJsonPointer(path);
if (parts.length >= 2 && parts[0] === 'properties') {
const rootProp = parts[1];
if (requiredProps.includes(rootProp)) {
requiredWriteOnlyProps.add(rootProp);
}
}
}

let tabstopIndex = 1;
for (const prop of requiredWriteOnlyProps) {
if (!(prop in resourceProperties) || this.isEmpty(resourceProperties[prop])) {
resourceProperties[prop] = `\${${tabstopIndex++}:update required write only property}`;
}
}
}

private isEmpty(value: unknown): boolean {
if (value === null || value === undefined) {
return true;
}
if (typeof value === 'object' && !Array.isArray(value)) {
return Object.keys(value as Record<string, unknown>).length === 0;
}
return false;
}

private parseJsonPointer(pointer: string): string[] {
if (!pointer.startsWith('/')) {
return [];
}
return pointer
.slice(1)
.split('/')
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
}
}
3 changes: 3 additions & 0 deletions src/schema/transformers/TransformersUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ResourceStatePurpose } from '../../resourceState/ResourceStateTypes';
import { AddWriteOnlyRequiredPropertiesTransformer } from './AddWriteOnlyRequiredPropertiesTransformer';
import { RemoveMutuallyExclusivePropertiesTransformer } from './RemoveMutuallyExclusivePropertiesTransformer';
import { RemoveReadonlyPropertiesTransformer } from './RemoveReadonlyPropertiesTransformer';
import { RemoveRequiredXorPropertiesTransformer } from './RemoveRequiredXorPropertiesTransformer';
Expand All @@ -14,6 +15,7 @@ export class TransformersUtil {
new RemoveMutuallyExclusivePropertiesTransformer(),
new RemoveSystemTagsTransformer(),
new RemoveRequiredXorPropertiesTransformer(),
new AddWriteOnlyRequiredPropertiesTransformer(),
];
} else if (purpose === ResourceStatePurpose.CLONE) {
return [
Expand All @@ -22,6 +24,7 @@ export class TransformersUtil {
new RemoveSystemTagsTransformer(),
new ReplacePrimaryIdentifierTransformer(),
new RemoveRequiredXorPropertiesTransformer(),
new AddWriteOnlyRequiredPropertiesTransformer(),
];
}
return [];
Expand Down
Loading
Loading