Compare commits

3 Commits

Author SHA1 Message Date
ssa
76b54cff3b Merge pull request 'feat' (#7) from feat into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Deploy to Server / deploy (push) Has been cancelled
Build Docker Image / build (push) Has been cancelled
Reviewed-on: #7
2026-02-20 15:45:41 +03:00
ssa
31d9e388f8 Merge pull request 'feat' (#6) from feat into main
Some checks failed
CI / build-and-test (push) Has been cancelled
Build Docker Image / build (push) Has been cancelled
Deploy to Server / deploy (push) Failing after 17m35s
Reviewed-on: #6
2026-02-20 13:58:50 +03:00
ssa
bc796f2aa4 Merge pull request 'rewrite' (#5) from rewrite into main
Reviewed-on: #5
2026-02-19 23:48:41 +03:00
8 changed files with 72 additions and 529 deletions

View File

@@ -98,7 +98,7 @@ function App() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
<main className="container mx-auto px-4 py-6 max-w-[1920px] min-h-[calc(100vh-48px)]">
<main className="container mx-auto px-4 py-6 max-w-7xl">
{/* Header */}
<div className="mb-6">
<FileChooser
@@ -112,10 +112,10 @@ function App() {
</div>
{envs.length > 0 ? (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 min-h-[calc(100vh-200px)]">
{/* Environment Panel - 5/12 width */}
<section className="lg:col-span-5 xl:col-span-5 2xl:col-span-5">
<div className="sticky top-6 h-full">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
{/* Environment Panel */}
<section className="lg:col-span-5 xl:col-span-4">
<div className="sticky top-6">
<Environment
envs={envs}
onChanged={async (e) => await handleEnvChanged(e)}
@@ -126,8 +126,8 @@ function App() {
</div>
</section>
{/* Content Panel - 7/12 width */}
<section className="lg:col-span-7 xl:col-span-7 2xl:col-span-7 h-full">
{/* Content Panel */}
<section className="lg:col-span-7 xl:col-span-8">
<Content
env={currentEnv}
config={config}

View File

@@ -14,13 +14,6 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
const [originalContent, setOriginalContent] = useState(config.template.content);
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)
useEffect(() => {
if (mode === 'view') {
@@ -41,82 +34,33 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
setMode('view');
}
function validateJson(value: string): boolean {
try {
if (!value.trim()) {
setJsonError(null);
return true;
}
// Step 1: Remove /* */ multi-line comments (safe, can't appear in strings)
let sanitized = value.replace(/\/\*[\s\S]*?\*\//g, '');
// 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);
return true;
} catch (e) {
setJsonError((e as Error).message);
return false;
}
}
function handleSave() {
// Validate JSON before saving (with comment and placeholder support)
if (validateJson(draftContent)) {
// Validate JSON before saving (with placeholder support)
try {
const sanitizedValue = draftContent.replace(/@[^@]+@/g, '1');
JSON.parse(sanitizedValue);
setJsonError(null);
onSaved(draftContent);
setMode('view');
} catch (e) {
setJsonError((e as Error).message);
}
}
function handleDraftChange(value: string) {
setDraftContent(value);
// Validate JSON on every change
validateJson(value);
try {
if (value.trim()) {
const sanitizedValue = value.replace(/@[^@]+@/g, '1');
JSON.parse(sanitizedValue);
setJsonError(null);
} else {
setJsonError(null);
}
} catch (e) {
setJsonError((e as Error).message);
}
}
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
@@ -141,18 +85,12 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
const isValidJson = jsonError === null;
return (
<div className="config-template-editor animate-fade-in h-full flex flex-col">
<div className="config-template-editor animate-fade-in">
{mode === 'view' ? (
<div className="space-y-3 flex flex-col h-full">
<div className="flex-shrink-0 flex items-center justify-between flex-wrap gap-3">
<div className="flex items-center gap-2 flex-wrap">
<div className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<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>
<Button
variant="primary"
@@ -163,14 +101,12 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
Edit Template
</Button>
</div>
<div className="flex-1 min-h-0">
<CodeBlock code={config.template.content || '{}'} language="json" maxHeight="100%" />
</div>
<CodeBlock code={config.template.content || '{}'} language="json" maxHeight="500px" />
</div>
) : (
<div className="space-y-3 flex flex-col h-full">
<div className="flex-shrink-0 flex items-center gap-2 flex-wrap">
<div className="space-y-3">
<div className="flex items-center gap-2 flex-wrap">
<Button
variant="success"
size="sm"
@@ -180,7 +116,7 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
>
Save Changes
</Button>
<Button
variant="secondary"
size="sm"
@@ -203,17 +139,10 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
</>
)}
</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>
{jsonError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 flex-shrink-0">
<div className="bg-red-50 border border-red-200 rounded-lg p-3">
<p className="text-sm text-red-700 font-mono">{jsonError}</p>
</div>
)}
@@ -223,8 +152,8 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
w-full p-3 font-mono text-sm rounded-lg border-2
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
transition-all duration-200
${isValidJson
? 'border-green-300 bg-green-50'
${isValidJson
? 'border-green-300 bg-green-50'
: 'border-red-300 bg-red-50'
}
`}
@@ -232,7 +161,7 @@ export function ConfigTemplateEditor({ config, onSaved }: ConfigTemplateEditorPr
onChange={(e) => handleDraftChange(e.target.value)}
onKeyDown={handleKeyDown}
rows={20}
style={{ whiteSpace: 'pre', overflowX: 'auto', flex: '1 1 auto', minHeight: '200px' }}
style={{ whiteSpace: 'pre', overflowX: 'auto' }}
spellCheck={false}
/>
</div>

View File

@@ -20,8 +20,8 @@ export function Content({ config, env, onTemplateSaved }: ContentProps) {
const tabs: Array<{ id: string; label: string; badge?: string | number; badgeVariant?: 'warning' | 'danger' }> = [
{ id: 'env', label: 'Env' },
{
id: 'template',
{
id: 'template',
label: 'Content Template',
badge: hasValidationWarnings ? '!' : undefined,
badgeVariant: hasValidationWarnings ? 'warning' : undefined,
@@ -31,24 +31,22 @@ export function Content({ config, env, onTemplateSaved }: ContentProps) {
];
return (
<div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden h-full flex flex-col">
<div className="flex-shrink-0">
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
</div>
<div className="flex-1 overflow-y-auto p-4 min-h-0">
<div className="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden">
<Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} />
<div className="p-4">
<TabPanel isActive={activeTab === 'env'}>
<ContentParams env={env} />
</TabPanel>
<TabPanel isActive={activeTab === 'template'}>
<ConfigTemplateEditor config={config} onSaved={onTemplateSaved} />
</TabPanel>
<TabPanel isActive={activeTab === 'raw'}>
<ContentRaw config={config} env={env} />
</TabPanel>
<TabPanel isActive={activeTab === 'test'}>
<ContentTest config={config} env={env} />
</TabPanel>
@@ -59,10 +57,10 @@ export function Content({ config, env, onTemplateSaved }: ContentProps) {
function ContentParams({ env }: { env: Env }) {
const xml = Builder.getEnv(env).build();
return (
<div className="animate-fade-in h-full">
<CodeBlock code={xml} language="xml" maxHeight="100%" />
<div className="animate-fade-in">
<CodeBlock code={xml} language="xml" maxHeight="500px" />
</div>
);
}
@@ -79,8 +77,8 @@ ${templateContent}
</engine>`;
return (
<div className="animate-fade-in h-full">
<CodeBlock code={xml} language="xml" maxHeight="100%" />
<div className="animate-fade-in">
<CodeBlock code={xml} language="xml" maxHeight="500px" />
</div>
);
}
@@ -97,8 +95,8 @@ function ContentTest({ config, env }: { config: Config; env: Env }) {
}));
return (
<div className="animate-fade-in space-y-4 h-full flex flex-col">
<div className="flex-shrink-0 flex items-center gap-2">
<div className="animate-fade-in space-y-4">
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-slate-700">Select Environment:</label>
<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"
@@ -110,10 +108,8 @@ function ContentTest({ config, env }: { config: Config; env: Env }) {
))}
</select>
</div>
<div className="flex-1 min-h-0">
<CodeBlock code={filledTemplate} language="json" maxHeight="100%" />
</div>
<CodeBlock code={filledTemplate} language="json" maxHeight="500px" />
</div>
);
}

View File

@@ -94,10 +94,10 @@ export function Environment({ envs, onChanged, onSelected, onAdd, onRemove }: En
));
return (
<Card variant="bordered" padding="none" className="h-full overflow-hidden flex flex-col">
<CardBody className="space-y-2 flex flex-col h-full overflow-hidden">
<Card variant="bordered" padding="none" className="h-full">
<CardBody className="space-y-1">
{/* Environment Selector */}
<div className="flex-shrink-0 flex gap-2">
<div className="flex gap-2">
<div className="flex-1">
<Select
label="Environment"
@@ -112,7 +112,7 @@ export function Environment({ envs, onChanged, onSelected, onAdd, onRemove }: En
/>
</div>
<div className="flex flex-col justify-center gap-2 pt-6 flex-shrink-0">
<div className="flex flex-col justify-center gap-2 pt-6">
<div className="flex gap-2">
<Button
variant="success"
@@ -134,13 +134,13 @@ export function Environment({ envs, onChanged, onSelected, onAdd, onRemove }: En
</div>
</div>
{/* Parameters Section - Scrollable */}
<div className="flex-1 overflow-hidden flex flex-col min-h-0">
<h3 className="text-sm font-semibold text-slate-700 mb-1 uppercase tracking-wide flex-shrink-0">
{/* Parameters Section */}
<div>
<h3 className="text-sm font-semibold text-slate-700 mb-1 uppercase tracking-wide">
Parameters
</h3>
<div className="flex-1 overflow-y-auto space-y-0 pr-2 -mr-2">
<div className="space-y-0">
{paramCtrls}
<EnvironmentParam

View File

@@ -48,7 +48,7 @@ export function EnvironmentParam({ param, onChanged, isNew }: EnvironmentParamPr
return (
<div
className={`
grid grid-cols-12 gap-1 p-1 rounded-lg transition-all duration-200
grid grid-cols-12 gap-2 p-2 rounded-lg transition-all duration-200
${isChangedClass}
${focusedClass ? 'bg-blue-50' : 'bg-white'}
hover:bg-slate-50
@@ -65,7 +65,7 @@ export function EnvironmentParam({ param, onChanged, isNew }: EnvironmentParamPr
className="text-sm"
/>
</div>
<div className="col-span-7">
<Input
value={localParam.value ?? ''}
@@ -77,7 +77,7 @@ export function EnvironmentParam({ param, onChanged, isNew }: EnvironmentParamPr
className="text-sm"
/>
</div>
<div className="col-span-1 flex items-center justify-center">
{isNew ? (
<Button

View File

@@ -97,48 +97,29 @@ export class Config {
* Updates template by adding placeholders for environment params
*/
public updateTemplateFromEnv(env: Env): void {
// If template is empty, initialize with empty object
if (!this.template.content || !this.template.content.trim()) {
this.template = new ConfigTemplate('{}');
}
let templateObj: Record<string, string> = {};
// Try to parse existing template
let templateObj: Record<string, any> = {};
let hasExistingContent = false;
try {
if (this.template.content.trim()) {
templateObj = JSON.parse(this.template.content);
hasExistingContent = Object.keys(templateObj).length > 0;
}
} catch {
// If invalid JSON, preserve the raw content and don't modify
return;
// Start fresh if invalid JSON
}
// Add placeholders for params that don't exist yet
let hasChanges = false;
for (const param of env.params) {
if (param.name && param.name.trim()) {
const paramName = param.name.trim();
const placeholder = `@${paramName}@`;
const templateKey = `!!! ${paramName}`;
const placeholder = `@${param.name}@`;
// Check if placeholder exists anywhere in template
if (!this.template.content.includes(placeholder)) {
// Only add if not already in templateObj
if (!templateObj[templateKey]) {
templateObj[templateKey] = placeholder;
hasChanges = true;
}
templateObj[`!!! ${param.name}`] = placeholder;
}
}
}
// 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));
}
/**

View File

@@ -1,159 +0,0 @@
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
});
});

View File

@@ -1,204 +0,0 @@
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);
});
});