Compare commits
5 Commits
9f51379df9
...
feat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7563b1f98 | ||
|
|
ecded86467 | ||
|
|
70e0545fef | ||
|
|
1beb9f2026 | ||
|
|
28fc3f9e21 |
@@ -14,6 +14,13 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
|
|||||||
const [originalContent, setOriginalContent] = useState(config.template.content);
|
const [originalContent, setOriginalContent] = useState(config.template.content);
|
||||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Validate placeholders for warning
|
||||||
|
const missingPlaceholders = config.validatePlaceholders();
|
||||||
|
const hasValidationWarnings = missingPlaceholders.length > 0;
|
||||||
|
const warningMessage = hasValidationWarnings
|
||||||
|
? `Missing params: ${missingPlaceholders.join(', ')}`
|
||||||
|
: '';
|
||||||
|
|
||||||
// Sync draft when config changes (only in view mode)
|
// Sync draft when config changes (only in view mode)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === 'view') {
|
if (mode === 'view') {
|
||||||
@@ -41,13 +48,55 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip comments (// single-line and /* */ multi-line) and placeholders
|
// Step 1: Remove /* */ multi-line comments (safe, can't appear in strings)
|
||||||
const sanitizedValue = value
|
let sanitized = value.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
|
|
||||||
.replace(/\/\/.*$/gm, '') // Remove // comments
|
|
||||||
.replace(/@[^@]+@/g, '1'); // Replace placeholders
|
|
||||||
|
|
||||||
JSON.parse(sanitizedValue);
|
// Step 2: Process line by line to handle // comments and placeholders
|
||||||
|
// Only remove // comments that are OUTSIDE of quoted strings
|
||||||
|
const lines = sanitized.split('\n');
|
||||||
|
const processedLines = lines.map(line => {
|
||||||
|
// Find // that's outside quotes
|
||||||
|
// Strategy: split by quotes, only process odd-indexed segments (outside quotes)
|
||||||
|
let result = '';
|
||||||
|
let inQuote = false;
|
||||||
|
let quoteChar = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
const prevChar = i > 0 ? line[i - 1] : '';
|
||||||
|
|
||||||
|
// Check for quote start/end (not escaped)
|
||||||
|
if ((char === '"' || char === "'") && prevChar !== '\\') {
|
||||||
|
if (!inQuote) {
|
||||||
|
inQuote = true;
|
||||||
|
quoteChar = char;
|
||||||
|
} else if (char === quoteChar) {
|
||||||
|
inQuote = false;
|
||||||
|
quoteChar = '';
|
||||||
|
}
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
// Check for // comment start (only outside quotes)
|
||||||
|
else if (!inQuote && char === '/' && line[i + 1] === '/') {
|
||||||
|
// Rest of line is comment, stop processing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
sanitized = processedLines.join('\n');
|
||||||
|
|
||||||
|
// Step 3: Replace unquoted @placeholders@ with dummy values
|
||||||
|
// Placeholders in JSON values are typically quoted: "@param@"
|
||||||
|
// We replace all @param@ with 1 (works for both quoted and unquoted)
|
||||||
|
sanitized = sanitized.replace(/@[^@]+@/g, '1');
|
||||||
|
|
||||||
|
JSON.parse(sanitized);
|
||||||
setJsonError(null);
|
setJsonError(null);
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -92,12 +141,18 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
|
|||||||
const isValidJson = jsonError === null;
|
const isValidJson = jsonError === null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="config-template-editor animate-fade-in">
|
<div className="config-template-editor animate-fade-in h-full flex flex-col">
|
||||||
{mode === 'view' ? (
|
{mode === 'view' ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 flex flex-col h-full">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex-shrink-0 flex items-center justify-between flex-wrap gap-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<Badge variant="success">View Mode</Badge>
|
<Badge variant="success">View Mode</Badge>
|
||||||
|
{hasValidationWarnings && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-amber-600 bg-amber-50 px-2.5 py-1 rounded-md">
|
||||||
|
<span className="font-medium">Warning:</span>
|
||||||
|
<span>{warningMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
@@ -109,11 +164,13 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CodeBlock code={config.template.content || '{}'} language="json" maxHeight="500px" />
|
<div className="flex-1 min-h-0">
|
||||||
|
<CodeBlock code={config.template.content || '{}'} language="json" maxHeight="100%" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 flex flex-col h-full">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex-shrink-0 flex items-center gap-2 flex-wrap">
|
||||||
<Button
|
<Button
|
||||||
variant="success"
|
variant="success"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -146,10 +203,17 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
|
{hasValidationWarnings && isValidJson && (
|
||||||
|
<div className="flex items-center gap-1.5 text-sm text-amber-600 bg-amber-50 px-2.5 py-1 rounded-md">
|
||||||
|
<span className="font-medium">Warning:</span>
|
||||||
|
<span>{warningMessage}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{jsonError && (
|
{jsonError && (
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3 flex-shrink-0">
|
||||||
<p className="text-sm text-red-700 font-mono">{jsonError}</p>
|
<p className="text-sm text-red-700 font-mono">{jsonError}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -168,7 +232,7 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
|
|||||||
onChange={(e) => handleDraftChange(e.target.value)}
|
onChange={(e) => handleDraftChange(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
rows={20}
|
rows={20}
|
||||||
style={{ whiteSpace: 'pre', overflowX: 'auto' }}
|
style={{ whiteSpace: 'pre', overflowX: 'auto', flex: '1 1 auto', minHeight: '200px' }}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ function ContentParams({ env }: { env: Env }) {
|
|||||||
const xml = Builder.getEnv(env).build();
|
const xml = Builder.getEnv(env).build();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in h-full">
|
||||||
<CodeBlock code={xml} language="xml" maxHeight="500px" />
|
<CodeBlock code={xml} language="xml" maxHeight="100%" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -79,8 +79,8 @@ ${templateContent}
|
|||||||
</engine>`;
|
</engine>`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in h-full">
|
||||||
<CodeBlock code={xml} language="xml" maxHeight="500px" />
|
<CodeBlock code={xml} language="xml" maxHeight="100%" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,8 +97,8 @@ function ContentTest({ config, env }: { config: Config; env: Env }) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="animate-fade-in space-y-4">
|
<div className="animate-fade-in space-y-4 h-full flex flex-col">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex-shrink-0 flex items-center gap-2">
|
||||||
<label className="text-sm font-medium text-slate-700">Select Environment:</label>
|
<label className="text-sm font-medium text-slate-700">Select Environment:</label>
|
||||||
<select
|
<select
|
||||||
className="px-3 py-1.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="px-3 py-1.5 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
@@ -111,7 +111,9 @@ function ContentTest({ config, env }: { config: Config; env: Env }) {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CodeBlock code={filledTemplate} language="json" maxHeight="500px" />
|
<div className="flex-1 min-h-0">
|
||||||
|
<CodeBlock code={filledTemplate} language="json" maxHeight="100%" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,30 +97,49 @@ export class Config {
|
|||||||
* Updates template by adding placeholders for environment params
|
* Updates template by adding placeholders for environment params
|
||||||
*/
|
*/
|
||||||
public updateTemplateFromEnv(env: Env): void {
|
public updateTemplateFromEnv(env: Env): void {
|
||||||
let templateObj: Record<string, string> = {};
|
// If template is empty, initialize with empty object
|
||||||
|
if (!this.template.content || !this.template.content.trim()) {
|
||||||
|
this.template = new ConfigTemplate('{}');
|
||||||
|
}
|
||||||
|
|
||||||
// Try to parse existing template
|
// Try to parse existing template
|
||||||
|
let templateObj: Record<string, any> = {};
|
||||||
|
let hasExistingContent = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.template.content.trim()) {
|
if (this.template.content.trim()) {
|
||||||
templateObj = JSON.parse(this.template.content);
|
templateObj = JSON.parse(this.template.content);
|
||||||
|
hasExistingContent = Object.keys(templateObj).length > 0;
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Start fresh if invalid JSON
|
// If invalid JSON, preserve the raw content and don't modify
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add placeholders for params that don't exist yet
|
// Add placeholders for params that don't exist yet
|
||||||
|
let hasChanges = false;
|
||||||
for (const param of env.params) {
|
for (const param of env.params) {
|
||||||
if (param.name && param.name.trim()) {
|
if (param.name && param.name.trim()) {
|
||||||
const placeholder = `@${param.name}@`;
|
const paramName = param.name.trim();
|
||||||
|
const placeholder = `@${paramName}@`;
|
||||||
|
const templateKey = `!!! ${paramName}`;
|
||||||
|
|
||||||
|
// Check if placeholder exists anywhere in template
|
||||||
if (!this.template.content.includes(placeholder)) {
|
if (!this.template.content.includes(placeholder)) {
|
||||||
templateObj[`!!! ${param.name}`] = placeholder;
|
// Only add if not already in templateObj
|
||||||
|
if (!templateObj[templateKey]) {
|
||||||
|
templateObj[templateKey] = placeholder;
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only update if there are actual changes
|
||||||
|
if (hasChanges || !hasExistingContent) {
|
||||||
this.template = new ConfigTemplate(JSON.stringify(templateObj, null, 4));
|
this.template = new ConfigTemplate(JSON.stringify(templateObj, null, 4));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that all template placeholders have corresponding params (backward compatibility)
|
* Validates that all template placeholders have corresponding params (backward compatibility)
|
||||||
|
|||||||
159
src/test/ConfigUpdate.test.ts
Normal file
159
src/test/ConfigUpdate.test.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { Config } from '../models/Config';
|
||||||
|
import { Env } from '../models/Env';
|
||||||
|
import { EnvParam } from '../models/EnvParam';
|
||||||
|
|
||||||
|
describe('Config Template Update', () => {
|
||||||
|
it('should preserve existing template when adding new parameter', () => {
|
||||||
|
// Arrange: Create config with existing template
|
||||||
|
const config = new Config();
|
||||||
|
const existingTemplate = `{
|
||||||
|
"existingKey": "existingValue",
|
||||||
|
"anotherKey": 123
|
||||||
|
}`;
|
||||||
|
config.setTemplate(existingTemplate);
|
||||||
|
|
||||||
|
const env = new Env(0, 'DEFAULT', [
|
||||||
|
new EnvParam(1, 'host', 'localhost'),
|
||||||
|
new EnvParam(2, 'port', '8080')
|
||||||
|
]);
|
||||||
|
config.setEnvs([env]);
|
||||||
|
|
||||||
|
// Act: Update template from env (simulating adding parameters)
|
||||||
|
config.updateTemplateFromEnv(env);
|
||||||
|
|
||||||
|
// Assert: Template should have both existing content and new placeholders
|
||||||
|
const templateContent = config.template.content;
|
||||||
|
const templateObj = JSON.parse(templateContent);
|
||||||
|
|
||||||
|
expect(templateObj.existingKey).toBe('existingValue');
|
||||||
|
expect(templateObj.anotherKey).toBe(123);
|
||||||
|
expect(templateObj['!!! host']).toBe('@host@');
|
||||||
|
expect(templateObj['!!! port']).toBe('@port@');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not duplicate placeholders that already exist', () => {
|
||||||
|
// Arrange: Create config with template that already has placeholder
|
||||||
|
const config = new Config();
|
||||||
|
const existingTemplate = `{
|
||||||
|
"host": "@host@",
|
||||||
|
"port": 8080
|
||||||
|
}`;
|
||||||
|
config.setTemplate(existingTemplate);
|
||||||
|
|
||||||
|
const env = new Env(0, 'DEFAULT', [
|
||||||
|
new EnvParam(1, 'host', 'localhost'), // Already in template
|
||||||
|
new EnvParam(2, 'port', '8080') // Already in template
|
||||||
|
]);
|
||||||
|
config.setEnvs([env]);
|
||||||
|
|
||||||
|
// Act: Update template
|
||||||
|
config.updateTemplateFromEnv(env);
|
||||||
|
|
||||||
|
// Assert: Should not add !!! entries for existing placeholders
|
||||||
|
const templateContent = config.template.content;
|
||||||
|
// Count occurrences of @host@
|
||||||
|
const hostCount = (templateContent.match(/@host@/g) || []).length;
|
||||||
|
expect(hostCount).toBe(1); // Only original, no duplicate
|
||||||
|
|
||||||
|
// Should preserve original structure
|
||||||
|
expect(templateContent).toContain('"host": "@host@"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize empty template', () => {
|
||||||
|
// Arrange: Create config with empty template
|
||||||
|
const config = new Config();
|
||||||
|
config.setTemplate('');
|
||||||
|
|
||||||
|
const env = new Env(0, 'DEFAULT', [
|
||||||
|
new EnvParam(1, 'host', 'localhost')
|
||||||
|
]);
|
||||||
|
config.setEnvs([env]);
|
||||||
|
|
||||||
|
// Act: Update template
|
||||||
|
config.updateTemplateFromEnv(env);
|
||||||
|
|
||||||
|
// Assert: Should create template with placeholder
|
||||||
|
const templateContent = config.template.content;
|
||||||
|
const templateObj = JSON.parse(templateContent);
|
||||||
|
expect(templateObj['!!! host']).toBe('@host@');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not modify invalid JSON template', () => {
|
||||||
|
// Arrange: Create config with invalid JSON template
|
||||||
|
const config = new Config();
|
||||||
|
const invalidTemplate = `{
|
||||||
|
"key": "value" // missing comma
|
||||||
|
"another": "key"
|
||||||
|
}`;
|
||||||
|
config.setTemplate(invalidTemplate);
|
||||||
|
const originalTemplate = config.template.content;
|
||||||
|
|
||||||
|
const env = new Env(0, 'DEFAULT', [
|
||||||
|
new EnvParam(1, 'host', 'localhost')
|
||||||
|
]);
|
||||||
|
config.setEnvs([env]);
|
||||||
|
|
||||||
|
// Act: Update template (should not modify invalid JSON)
|
||||||
|
config.updateTemplateFromEnv(env);
|
||||||
|
|
||||||
|
// Assert: Template should remain unchanged
|
||||||
|
expect(config.template.content).toBe(originalTemplate);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add multiple new placeholders to existing template', () => {
|
||||||
|
// Arrange
|
||||||
|
const config = new Config();
|
||||||
|
const existingTemplate = `{
|
||||||
|
"name": "myapp"
|
||||||
|
}`;
|
||||||
|
config.setTemplate(existingTemplate);
|
||||||
|
|
||||||
|
const env = new Env(0, 'DEFAULT', [
|
||||||
|
new EnvParam(1, 'host', 'localhost'),
|
||||||
|
new EnvParam(2, 'port', '8080'),
|
||||||
|
new EnvParam(3, 'user', 'admin')
|
||||||
|
]);
|
||||||
|
config.setEnvs([env]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
config.updateTemplateFromEnv(env);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const templateObj = JSON.parse(config.template.content);
|
||||||
|
expect(templateObj.name).toBe('myapp');
|
||||||
|
expect(templateObj['!!! host']).toBe('@host@');
|
||||||
|
expect(templateObj['!!! port']).toBe('@port@');
|
||||||
|
expect(templateObj['!!! user']).toBe('@user@');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only add placeholders for params not in template', () => {
|
||||||
|
// Arrange: Template has @host@ but not @port@
|
||||||
|
const config = new Config();
|
||||||
|
config.setTemplate(`{
|
||||||
|
"host": "@host@",
|
||||||
|
"name": "test"
|
||||||
|
}`);
|
||||||
|
|
||||||
|
const env = new Env(0, 'DEFAULT', [
|
||||||
|
new EnvParam(1, 'host', 'localhost'), // In template
|
||||||
|
new EnvParam(2, 'port', '8080') // Not in template
|
||||||
|
]);
|
||||||
|
config.setEnvs([env]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
config.updateTemplateFromEnv(env);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
const templateContent = config.template.content;
|
||||||
|
const templateObj = JSON.parse(templateContent);
|
||||||
|
|
||||||
|
// Should preserve original
|
||||||
|
expect(templateObj.host).toBe('@host@');
|
||||||
|
expect(templateObj.name).toBe('test');
|
||||||
|
|
||||||
|
// Should add only missing
|
||||||
|
expect(templateObj['!!! port']).toBe('@port@');
|
||||||
|
expect(templateObj['!!! host']).toBeUndefined(); // Not added, already exists
|
||||||
|
});
|
||||||
|
});
|
||||||
204
src/test/JsonValidation.test.ts
Normal file
204
src/test/JsonValidation.test.ts
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function that mimics the validation logic in ConfigTemplate.tsx
|
||||||
|
* This is the improved version with comment support
|
||||||
|
*/
|
||||||
|
function validateJsonWithComments(value: string): { valid: boolean; error?: string } {
|
||||||
|
try {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: Remove /* */ multi-line comments
|
||||||
|
let sanitized = value.replace(/\/\*[\s\S]*?\*\//g, '');
|
||||||
|
|
||||||
|
// Step 2: Process line by line to handle // comments outside quotes
|
||||||
|
const lines = sanitized.split('\n');
|
||||||
|
const processedLines = lines.map(line => {
|
||||||
|
let result = '';
|
||||||
|
let inQuote = false;
|
||||||
|
let quoteChar = '';
|
||||||
|
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
const char = line[i];
|
||||||
|
const prevChar = i > 0 ? line[i - 1] : '';
|
||||||
|
|
||||||
|
// Check for quote start/end (not escaped)
|
||||||
|
if ((char === '"' || char === "'") && prevChar !== '\\') {
|
||||||
|
if (!inQuote) {
|
||||||
|
inQuote = true;
|
||||||
|
quoteChar = char;
|
||||||
|
} else if (char === quoteChar) {
|
||||||
|
inQuote = false;
|
||||||
|
quoteChar = '';
|
||||||
|
}
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
// Check for // comment start (only outside quotes)
|
||||||
|
else if (!inQuote && char === '/' && line[i + 1] === '/') {
|
||||||
|
break; // Rest of line is comment
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
sanitized = processedLines.join('\n');
|
||||||
|
|
||||||
|
// Step 3: Replace @placeholders@ with dummy values
|
||||||
|
sanitized = sanitized.replace(/@[^@]+@/g, '1');
|
||||||
|
|
||||||
|
JSON.parse(sanitized);
|
||||||
|
return { valid: true };
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, error: (e as Error).message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('JSON Validation - Comment Support', () => {
|
||||||
|
it('should remove single-line // comments outside quotes', () => {
|
||||||
|
const json = `{
|
||||||
|
"key": "value" // this is a comment
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve // inside quoted strings', () => {
|
||||||
|
const json = `{
|
||||||
|
"url": "http://example.com/path",
|
||||||
|
"description": "This // that // other"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve URLs with // in values', () => {
|
||||||
|
const json = `{
|
||||||
|
"apiUrl": "http://localhost:8080/api",
|
||||||
|
"cdn": "https://cdn.example.com/assets"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove multi-line /* */ comments', () => {
|
||||||
|
const json = `{
|
||||||
|
/* This is a
|
||||||
|
multi-line comment */
|
||||||
|
"key": "value"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle mixed comments and placeholders', () => {
|
||||||
|
const json = `{
|
||||||
|
// Host configuration
|
||||||
|
"host": "@host@",
|
||||||
|
"port": @port@, // unquoted placeholder
|
||||||
|
/* API settings */
|
||||||
|
"apiUrl": "http://@host@:@port@/api" // URL with placeholders
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle comment-only lines', () => {
|
||||||
|
const json = `{
|
||||||
|
// Just a comment
|
||||||
|
"key": "value"
|
||||||
|
// Another comment
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should preserve // in string values with special characters', () => {
|
||||||
|
const json = `{
|
||||||
|
"regex": "pattern // with slashes",
|
||||||
|
"path": "C://Program Files//App",
|
||||||
|
"comment": "text // more text // even more"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle escaped quotes correctly', () => {
|
||||||
|
const json = `{
|
||||||
|
"message": "He said \\"hello // world\\""
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject invalid JSON with comments', () => {
|
||||||
|
const json = `{
|
||||||
|
"key": "value" // missing comma
|
||||||
|
"another": "key"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(false);
|
||||||
|
expect(result.error).toContain('Expected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty template', () => {
|
||||||
|
const result = validateJsonWithComments('');
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle whitespace only', () => {
|
||||||
|
const result = validateJsonWithComments(' \n\t ');
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JSON Validation - Placeholder Support', () => {
|
||||||
|
it('should validate quoted placeholders', () => {
|
||||||
|
const json = `{
|
||||||
|
"Host": "@host@",
|
||||||
|
"Port": "@port@"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate unquoted placeholders', () => {
|
||||||
|
const json = `{
|
||||||
|
"Host": "@host@",
|
||||||
|
"Port": @port@
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate placeholders in URLs', () => {
|
||||||
|
const json = `{
|
||||||
|
"ApiUrl": "http://@host@:@port@/api/v1"
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate complex template with comments and placeholders', () => {
|
||||||
|
const json = `{
|
||||||
|
// Database config
|
||||||
|
"db": {
|
||||||
|
"host": "@db_host@",
|
||||||
|
"port": @db_port@,
|
||||||
|
"url": "jdbc:mysql://@db_host@:@db_port@/mydb" // connection string
|
||||||
|
},
|
||||||
|
/* API settings */
|
||||||
|
"api": {
|
||||||
|
"baseUrl": "https://@api_host@/v1",
|
||||||
|
"timeout": @timeout@
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
const result = validateJsonWithComments(json);
|
||||||
|
expect(result.valid).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user