mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-28 18:33:37 +00:00
fix(config): support uri formats in schema validation
This commit is contained in:
@@ -40,6 +40,9 @@ type AllowedValuesCollection = {
|
||||
hasValues: boolean;
|
||||
};
|
||||
|
||||
const CUSTOM_EXPECTED_ONE_OF_RE = /expected one of ((?:"[^"]+"(?:\|"?[^"]+"?)*)+)/i;
|
||||
const STREAMING_ALLOWED_VALUES = [true, false, "off", "partial", "block", "progress"] as const;
|
||||
|
||||
function toIssueRecord(value: unknown): UnknownIssueRecord | null {
|
||||
if (!value || typeof value !== "object") {
|
||||
return null;
|
||||
@@ -47,6 +50,28 @@ function toIssueRecord(value: unknown): UnknownIssueRecord | null {
|
||||
return value as UnknownIssueRecord;
|
||||
}
|
||||
|
||||
function collectAllowedValuesFromCustomIssue(record: UnknownIssueRecord): AllowedValuesCollection {
|
||||
const message = typeof record.message === "string" ? record.message : "";
|
||||
const expectedMatch = message.match(CUSTOM_EXPECTED_ONE_OF_RE);
|
||||
if (expectedMatch?.[1]) {
|
||||
const values = [...expectedMatch[1].matchAll(/"([^"]+)"/g)].map((match) => match[1]);
|
||||
return { values, incomplete: false, hasValues: values.length > 0 };
|
||||
}
|
||||
|
||||
const path = Array.isArray(record.path)
|
||||
? record.path.filter((segment): segment is string => typeof segment === "string")
|
||||
: [];
|
||||
if (path.at(-1) === "streaming") {
|
||||
return {
|
||||
values: [...STREAMING_ALLOWED_VALUES],
|
||||
incomplete: false,
|
||||
hasValues: true,
|
||||
};
|
||||
}
|
||||
|
||||
return { values: [], incomplete: false, hasValues: false };
|
||||
}
|
||||
|
||||
function collectAllowedValuesFromIssue(issue: unknown): AllowedValuesCollection {
|
||||
const record = toIssueRecord(issue);
|
||||
if (!record) {
|
||||
@@ -70,6 +95,10 @@ function collectAllowedValuesFromIssue(issue: unknown): AllowedValuesCollection
|
||||
return { values: [], incomplete: true, hasValues: false };
|
||||
}
|
||||
|
||||
if (code === "custom") {
|
||||
return collectAllowedValuesFromCustomIssue(record);
|
||||
}
|
||||
|
||||
if (code !== "invalid_union") {
|
||||
return { values: [], incomplete: false, hasValues: false };
|
||||
}
|
||||
|
||||
@@ -231,4 +231,43 @@ describe("schema validator", () => {
|
||||
expect(issue?.text).not.toContain("\x1b");
|
||||
}
|
||||
});
|
||||
|
||||
it("supports uri-formatted string schemas", () => {
|
||||
const valid = validateJsonSchemaValue({
|
||||
cacheKey: "schema-validator.test.uri.valid",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiRoot: {
|
||||
type: "string",
|
||||
format: "uri",
|
||||
},
|
||||
},
|
||||
required: ["apiRoot"],
|
||||
},
|
||||
value: { apiRoot: "https://api.telegram.org" },
|
||||
});
|
||||
expect(valid.ok).toBe(true);
|
||||
|
||||
const invalid = validateJsonSchemaValue({
|
||||
cacheKey: "schema-validator.test.uri.invalid",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
apiRoot: {
|
||||
type: "string",
|
||||
format: "uri",
|
||||
},
|
||||
},
|
||||
required: ["apiRoot"],
|
||||
},
|
||||
value: { apiRoot: "not a uri" },
|
||||
});
|
||||
expect(invalid.ok).toBe(false);
|
||||
if (!invalid.ok) {
|
||||
expect(invalid.errors.find((entry) => entry.path === "apiRoot")?.message).toContain(
|
||||
"must match format",
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,15 @@ import { sanitizeTerminalText } from "../terminal/safe-text.js";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
type AjvLike = {
|
||||
addFormat: (
|
||||
name: string,
|
||||
format:
|
||||
| RegExp
|
||||
| {
|
||||
type?: string;
|
||||
validate: (value: string) => boolean;
|
||||
},
|
||||
) => AjvLike;
|
||||
compile: (schema: Record<string, unknown>) => ValidateFunction;
|
||||
};
|
||||
const ajvSingletons = new Map<"default" | "defaults", AjvLike>();
|
||||
@@ -25,6 +34,19 @@ function getAjv(mode: "default" | "defaults"): AjvLike {
|
||||
removeAdditional: false,
|
||||
...(mode === "defaults" ? { useDefaults: true } : {}),
|
||||
});
|
||||
instance.addFormat("uri", {
|
||||
type: "string",
|
||||
validate: (value: string) => {
|
||||
try {
|
||||
// Accept absolute URIs so generated config schemas can keep JSON Schema
|
||||
// `format: "uri"` without noisy AJV warnings during validation/build.
|
||||
new URL(value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
ajvSingletons.set(mode, instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user