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
4 changes: 4 additions & 0 deletions scripts/e2e/run-playwright-smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,10 @@ async function assertMobileNavigationKeyboardPath(browser, baseUrl) {

const menuTrigger = page.getByRole("button", { name: /Open Navigation/i }).first();
await menuTrigger.waitFor({ state: "visible", timeout: 15_000 });
await page.waitForFunction(() => {
const buttons = Array.from(document.querySelectorAll("button"));
return buttons.some((button) => /Open Navigation/i.test(button.textContent || button.getAttribute("aria-label") || "") && !button.disabled);
}, null, { timeout: 15_000 });
await menuTrigger.focus();
await page.keyboard.press("Enter");

Expand Down
7 changes: 6 additions & 1 deletion src/components/layout/deferred-mobile-nav-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import * as React from "react"
import dynamic from "next/dynamic"
import { useHasMounted } from "@/hooks/use-has-mounted"

const NavbarMobileMenu = dynamic(
() => import("./navbar-mobile-menu").then((mod) => mod.NavbarMobileMenu),
Expand All @@ -12,6 +13,7 @@ export function DeferredMobileNavMenu({
}: {
menuLabel: string
}) {
const hasMounted = useHasMounted()
const [isMounted, setIsMounted] = React.useState(false)
const [isOpen, setIsOpen] = React.useState(false)
const triggerRef = React.useRef<HTMLButtonElement>(null)
Expand All @@ -24,11 +26,13 @@ export function DeferredMobileNavMenu({
}, [])

const mountMenu = React.useCallback(() => {
if (!hasMounted) return

React.startTransition(() => {
setIsMounted(true)
})
setIsOpen(true)
}, [])
}, [hasMounted])

return (
<>
Expand All @@ -38,6 +42,7 @@ export function DeferredMobileNavMenu({
className="inline-flex h-11 w-11 items-center justify-center rounded-lg text-sm font-medium transition-[color,background-color,border-color,box-shadow,opacity,transform] outline-none hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 lg:hidden dark:hover:bg-accent/50"
onClick={isMounted ? () => setIsOpen(true) : mountMenu}
aria-expanded={isMounted ? isOpen : undefined}
disabled={!hasMounted}
>
<span aria-hidden="true" className="flex flex-col items-center justify-center gap-1">
<span className="block h-0.5 w-4 rounded-full bg-current" />
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@
},
"json_schema_workbench": {
"title": "JSON-Schema-Workbench",
"description": "Erzeuge Starter-Schemas aus JSON und validiere Payloads gegen eingefügte Schemas lokal im Browser.",
"description": "Erzeuge Starter-Schemas aus JSON und fuehre grundlegende unterstuetzte Schema-Pruefungen lokal im Browser aus.",
"mode_label": "JSON-Schema-Modus",
"mode_generate": "Schema erzeugen",
"mode_validate": "Payload validieren",
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3041,7 +3041,7 @@
},
"json_schema_workbench": {
"title": "JSON Schema Workbench",
"description": "Generate starter schemas from JSON and validate payloads against pasted schemas locally in your browser.",
"description": "Generate starter schemas from JSON and run basic supported schema checks locally in your browser.",
"mode_label": "JSON Schema mode",
"mode_generate": "Generate schema",
"mode_validate": "Validate payload",
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@
},
"json_schema_workbench": {
"title": "Atelier JSON Schema",
"description": "Générez des schemas de départ depuis du JSON et validez des payloads avec des schemas collés localement dans le navigateur.",
"description": "Generez des schemas de depart depuis du JSON et executez des controles Schema de base pris en charge localement dans le navigateur.",
"mode_label": "Mode JSON Schema",
"mode_generate": "Générer le schema",
"mode_validate": "Valider le payload",
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@
},
"json_schema_workbench": {
"title": "JSON Schema ワークベンチ",
"description": "ブラウザ内で JSON から初期 Schema を生成し、貼り付けた Schema で payload を検証します。",
"description": "ブラウザ内で JSON から初期 Schema を生成し、対応済みの基本 Schema チェックを実行します。",
"mode_label": "JSON Schema モード",
"mode_generate": "Schema を生成",
"mode_validate": "payload を検証",
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@
},
"json_schema_workbench": {
"title": "JSON Schema 워크벤치",
"description": "브라우저에서 JSON으로 시작 Schema를 만들고 붙여 넣은 Schema로 payload를 검증합니다.",
"description": "브라우저에서 JSON으로 시작 Schema를 만들고 지원되는 기본 Schema 검사를 실행합니다.",
"mode_label": "JSON Schema 모드",
"mode_generate": "Schema 생성",
"mode_validate": "payload 검증",
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@
},
"json_schema_workbench": {
"title": "JSON Schema 工作台",
"description": "在浏览器本地从 JSON 生成起始 Schema,并用粘贴的 Schema 校验 payload。",
"description": "在浏览器本地从 JSON 生成起始 Schema,并运行基础受支持的 Schema 检查。",
"mode_label": "JSON Schema 模式",
"mode_generate": "生成 Schema",
"mode_validate": "校验 payload",
Expand Down
2 changes: 1 addition & 1 deletion src/core/i18n/translations/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -3021,7 +3021,7 @@
},
"json_schema_workbench": {
"title": "JSON Schema 工作台",
"description": "在瀏覽器本機從 JSON 產生起始 Schema,並用貼上的 Schema 驗證 payload。",
"description": "在瀏覽器本機從 JSON 產生起始 Schema,並執行基礎支援的 Schema 檢查。",
"mode_label": "JSON Schema 模式",
"mode_generate": "產生 Schema",
"mode_validate": "驗證 payload",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5106,10 +5106,10 @@ export const TOP_TOOL_CONTENT_TEMPLATES: Record<string, ToolContentTemplateData>
},
"json-schema-workbench": {
toolKey: "json_schema_workbench",
intro: "Generate starter JSON Schema documents from representative payloads and validate JSON against pasted schemas locally, with path-level errors that make API contract drift easier to review.",
intro: "Generate starter JSON Schema documents from representative payloads and run basic supported schema checks locally, with path-level errors and unsupported-keyword warnings that make API contract drift easier to review.",
whatThisToolDoes: [
"It infers a practical Draft 2020-12 starter schema from objects, arrays, primitives, and nested payload samples.",
"It validates pasted JSON against a schema and reports each problem with a concrete JSON-style path and fix hint.",
"It runs basic supported schema checks against pasted JSON and warns when schemas use keywords this lightweight mode does not enforce.",
"It supports both direct tool usage and Pipeline Builder steps for schema generation and validation handoffs.",
"It keeps payloads and schemas in the browser, which is useful when contract samples include staging fields or private identifiers.",
],
Expand Down
26 changes: 25 additions & 1 deletion src/features/tools/json-schema-workbench/logic.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest"
import { generateJsonSchema, validateJsonWithSchema } from "./logic"
import { formatValidationReport, generateJsonSchema, validateJsonWithSchema } from "./logic"

describe("json-schema-workbench logic", () => {
it("generates a starter schema from nested JSON", () => {
Expand All @@ -20,4 +20,28 @@ describe("json-schema-workbench logic", () => {
it("accepts valid payloads", () => {
expect(validateJsonWithSchema('{"id":1}', '{"type":"object","required":["id"],"properties":{"id":{"type":"integer"}}}').valid).toBe(true)
})

it("warns when basic validation mode sees unsupported validation keywords", () => {
const report = validateJsonWithSchema(
'{"email":"not-an-email","slug":"INVALID SPACE","tags":[]}',
JSON.stringify({
type: "object",
properties: {
email: { type: "string", format: "email" },
slug: { type: "string", pattern: "^[a-z0-9-]+$" },
tags: { type: "array", minItems: 1 },
owner: { $ref: "#/definitions/User" },
},
definitions: {
User: { type: "object" },
},
}),
)

expect(report.valid).toBe(true)
expect(report.warnings.map((warning) => warning.keyword).sort()).toEqual(["$ref", "format", "minItems", "pattern"])
expect(report.summary).toContain("supported schema checks")
expect(report.summary).toContain("4 warning")
expect(formatValidationReport(report)).toContain("Basic mode does not enforce")
})
})
84 changes: 79 additions & 5 deletions src/features/tools/json-schema-workbench/logic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,26 @@
import type { JsonSchemaValidationIssue, JsonSchemaValidationReport, JsonSchemaValue } from "./types"
import type { JsonSchemaValidationIssue, JsonSchemaValidationReport, JsonSchemaValidationWarning, JsonSchemaValue } from "./types"

const SUPPORTED_SCHEMA_KEYWORDS = new Set([
"$schema",
"additionalProperties",
"enum",
"items",
"maximum",
"maxLength",
"minimum",
"minLength",
"properties",
"required",
"type",
])

const NON_VALIDATION_CONTAINER_KEYWORDS = new Set([
"$defs",
"definitions",
"description",
"examples",
"title",
])

function isPlainObject(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value)
Expand Down Expand Up @@ -70,6 +92,47 @@ function addIssue(issues: JsonSchemaValidationIssue[], path: string, message: st
issues.push({ path: path || "$", message, fix })
}

function addWarning(warnings: JsonSchemaValidationWarning[], path: string, keyword: string) {
if (warnings.some((warning) => warning.path === path && warning.keyword === keyword)) return
warnings.push({
path: path || "$",
keyword,
message: `Basic mode does not enforce "${keyword}" at ${path || "$"}.`,
})
}

function discoverUnsupportedKeywords(schema: unknown, path: string, warnings: JsonSchemaValidationWarning[]) {
if (!isPlainObject(schema)) return

for (const [keyword, value] of Object.entries(schema)) {
if (!SUPPORTED_SCHEMA_KEYWORDS.has(keyword) && !NON_VALIDATION_CONTAINER_KEYWORDS.has(keyword)) {
addWarning(warnings, path, keyword)
}

if (keyword === "properties" && isPlainObject(value)) {
for (const [propertyName, propertySchema] of Object.entries(value)) {
discoverUnsupportedKeywords(propertySchema, pathJoin(path, propertyName), warnings)
}
continue
}

if (keyword === "items") {
if (Array.isArray(value)) {
value.forEach((itemSchema, index) => discoverUnsupportedKeywords(itemSchema, pathJoin(path, `[${index}]`), warnings))
} else {
discoverUnsupportedKeywords(value, pathJoin(path, "[]"), warnings)
}
continue
}

if ((keyword === "$defs" || keyword === "definitions") && isPlainObject(value)) {
for (const [definitionName, definitionSchema] of Object.entries(value)) {
discoverUnsupportedKeywords(definitionSchema, pathJoin(path, definitionName), warnings)
}
}
}
}

function validateAgainstSchema(value: unknown, schema: JsonSchemaValue, path: string, issues: JsonSchemaValidationIssue[]) {
if (!acceptsType(schema, value)) {
addIssue(issues, path, `Expected ${Array.isArray(schema.type) ? schema.type.join(" or ") : schema.type}, received ${inferType(value)}.`, "Change the payload value or update the schema type.")
Expand Down Expand Up @@ -126,20 +189,31 @@ export function validateJsonWithSchema(payloadInput: string, schemaInput: string
const payload = JSON.parse(payloadInput)
const schema = JSON.parse(schemaInput) as JsonSchemaValue
const issues: JsonSchemaValidationIssue[] = []
const warnings: JsonSchemaValidationWarning[] = []
discoverUnsupportedKeywords(schema, "$", warnings)
validateAgainstSchema(payload, schema, "$", issues)
return {
valid: issues.length === 0,
issues,
summary: issues.length === 0 ? "Payload matches the schema." : `${issues.length} schema issue(s) found.`,
warnings,
summary: issues.length === 0
? warnings.length === 0
? "Payload matches the supported schema checks."
: `Payload matches the supported schema checks, with ${warnings.length} warning(s) for unsupported keyword(s).`
: `${issues.length} schema issue(s) found${warnings.length > 0 ? `, with ${warnings.length} warning(s) for unsupported keyword(s)` : ""}.`,
}
}

export function formatValidationReport(report: JsonSchemaValidationReport): string {
if (report.valid) return report.summary
return [report.summary, ...report.issues.map((issue) => `${issue.path}: ${issue.message} Fix: ${issue.fix}`)].join("\n")
const warningLines = report.warnings.map((warning) => `${warning.path}: ${warning.message}`)
if (report.valid) return [report.summary, ...warningLines].join("\n")
return [
report.summary,
...report.issues.map((issue) => `${issue.path}: ${issue.message} Fix: ${issue.fix}`),
...warningLines,
].join("\n")
}

export function runTool(input: string): string {
return generateJsonSchema(input)
}

4 changes: 2 additions & 2 deletions src/features/tools/json-schema-workbench/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const toolManifest = {
},
sampleInput: "{\"user\":{\"id\":1001,\"email\":\"alice@example.com\",\"active\":true}}",
sampleMode: "generate-schema",
keywords: ["json schema generator", "json schema validator", "json schema workbench", "validate json schema"],
searchKeywords: ["schema", "json schema", "validator", "draft 7", "contract"],
keywords: ["json schema generator", "basic json schema checks", "json schema workbench", "validate json schema"],
searchKeywords: ["schema", "json schema", "basic validator", "contract"],
persistInput: false,
networkAccess: "none",
} satisfies ToolMeta
8 changes: 8 additions & 0 deletions src/features/tools/json-schema-workbench/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type JsonSchemaValue = {
$ref?: string
type?: string | string[]
properties?: Record<string, JsonSchemaValue>
required?: string[]
Expand All @@ -9,6 +10,7 @@ export type JsonSchemaValue = {
maximum?: number
minLength?: number
maxLength?: number
[keyword: string]: unknown
}

export type JsonSchemaValidationIssue = {
Expand All @@ -20,6 +22,12 @@ export type JsonSchemaValidationIssue = {
export type JsonSchemaValidationReport = {
valid: boolean
issues: JsonSchemaValidationIssue[]
warnings: JsonSchemaValidationWarning[]
summary: string
}

export type JsonSchemaValidationWarning = {
path: string
keyword: string
message: string
}
Loading