Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

imp(EMP-2177): Add schema validation for JSON #22

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
23 changes: 23 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import fs from "fs-extra";
import path from "path";
import { input, select, number, expand } from "@inquirer/prompts";
import crypto from 'crypto';
import { validateComponent } from './schemas.js';

const configPath = path.join(process.env.HOME, ".ecli", "config.json");
const componentsDir = path.join(process.cwd(), "Components");
Expand Down Expand Up @@ -274,6 +275,12 @@ async function newComponent() {
name: await input({
message: "Enter the name of the component",
required: true,
validate: (input) => {
if (!/^[a-z0-9-]+$/.test(input)) {
return 'Component name must contain only lowercase letters, numbers, and hyphens';
}
return true;
}
}),
label: await input({
message: "Enter the label of the component",
Expand All @@ -290,6 +297,12 @@ async function newComponent() {
output_mime_type: await input({
message: "Enter the output mime type",
initial: "image/png",
validate: (input) => {
if (!/^[a-z]+\/[a-z0-9.+-]+$/.test(input)) {
return 'Invalid MIME type format (e.g., image/png, video/mp4)';
}
return true;
}
}),
type: await select({
message: "Select the type of the component",
Expand Down Expand Up @@ -420,6 +433,16 @@ async function applyComponents(componentName, options = { verbose: false }) {
return;
}

// Validate component files
const validationResult = await validateComponent(path.join(componentsDir, componentName), component.type);
if (!validationResult.valid) {
console.error(chalk.red('Validation errors:'));
validationResult.errors.forEach(error => {
console.error(chalk.red(`- ${error}`));
});
return;
}

// Check for required files based on component type
let requiredFiles = [];

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"@inquirer/prompts": "^7.0.1",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"fs-extra": "^11.2.0"
"fs-extra": "^11.2.0",
"zod": "^3.22.4"
},
"bin": {
"ecli": "./index.js"
Expand Down
153 changes: 153 additions & 0 deletions schemas.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { z } from 'zod';

// Base schemas for common field properties
const baseFieldSchema = z.object({
id: z.string(),
name: z.string(),
type: z.string(),
display: z.boolean(),
default: z.any().optional(),
});

// Schema for slider constraints
const sliderConstraintsSchema = z.object({
min: z.number(),
max: z.number(),
step: z.number(),
});

// Schema for different field types
const selectFieldSchema = baseFieldSchema.extend({
type: z.literal('select'),
conf_file: z.string(),
});

const promptEditorFieldSchema = baseFieldSchema.extend({
type: z.literal('prompt_editor'),
placeholder: z.string().optional(),
});

const sliderFieldSchema = baseFieldSchema.extend({
type: z.literal('slider'),
constraints: sliderConstraintsSchema,
});

const imageLoaderFieldSchema = baseFieldSchema.extend({
type: z.literal('image_loader'),
});

const aspectRatioFieldSchema = baseFieldSchema.extend({
type: z.literal('aspect_ratio'),
conf_file: z.string(),
});

// Union type for all field types
const fieldSchema = z.discriminatedUnion('type', [
selectFieldSchema,
promptEditorFieldSchema,
sliderFieldSchema,
imageLoaderFieldSchema,
aspectRatioFieldSchema,
]);

// Schema for form.json
export const formConfigSchema = z.object({
main: z.array(fieldSchema),
advanced: z.array(fieldSchema),
});

// Schema for inputs.json
export const inputConfigSchema = z.array(
z.object({
id: z.string(),
pathJq: z.string(),
})
);

// Schema for api.json
export const apiConfigSchema = z.object({
url: z.string().url(),
method: z.enum(['GET', 'POST', 'PUT', 'DELETE']),
headers: z.record(z.string()),
successResponseCode: z.array(z.number()),
fetchType: z.enum(['wait', 'poll']),
wait: z.object({
outputExprJq: z.string(),
}).optional(),
poll: z.object({
interval: z.number(),
timeout: z.number(),
statusExprJq: z.string(),
outputExprJq: z.string(),
}).optional(),
});

// Schema for workflow.json (comfy_workflow type)
export const workflowConfigSchema = z.object({
nodes: z.record(z.any()),
output_node_id: z.string(),
}).optional();

// Function to validate component files
export async function validateComponent(componentPath, type) {
const errors = [];

try {
// Validate form.json (required for all types)
const formPath = path.join(componentPath, 'form.json');
if (await fs.pathExists(formPath)) {
const formData = await fs.readJson(formPath);
try {
formConfigSchema.parse(formData);
} catch (e) {
errors.push(`form.json validation failed: ${e.message}`);
}
} else {
errors.push('form.json is required but missing');
}

// Validate type-specific files
if (type === 'fetch_api') {
// Validate api.json
const apiPath = path.join(componentPath, 'api.json');
if (await fs.pathExists(apiPath)) {
const apiData = await fs.readJson(apiPath);
try {
apiConfigSchema.parse(apiData);
} catch (e) {
errors.push(`api.json validation failed: ${e.message}`);
}
} else {
errors.push('api.json is required for fetch_api type but missing');
}

// Validate inputs.json
const inputsPath = path.join(componentPath, 'inputs.json');
if (await fs.pathExists(inputsPath)) {
const inputsData = await fs.readJson(inputsPath);
try {
inputConfigSchema.parse(inputsData);
} catch (e) {
errors.push(`inputs.json validation failed: ${e.message}`);
}
}
} else if (type === 'comfy_workflow') {
// Validate workflow.json
const workflowPath = path.join(componentPath, 'workflow.json');
if (await fs.pathExists(workflowPath)) {
const workflowData = await fs.readJson(workflowPath);
try {
workflowConfigSchema.parse(workflowData);
} catch (e) {
errors.push(`workflow.json validation failed: ${e.message}`);
}
} else {
errors.push('workflow.json is required for comfy_workflow type but missing');
}
}

return errors.length > 0 ? { valid: false, errors } : { valid: true };
} catch (error) {
return { valid: false, errors: [`Validation failed: ${error.message}`] };
}
}