diff --git a/nodepkg/jsoncue/package.json b/nodepkg/jsoncue/package.json index f5e246dc..f461054b 100644 --- a/nodepkg/jsoncue/package.json +++ b/nodepkg/jsoncue/package.json @@ -1,6 +1,6 @@ { "name": "@innoai-tech/jsoncue", - "version": "0.1.4", + "version": "0.1.8", "monobundle": { "build": { "clean": true diff --git a/nodepkg/jsoncue/src/__tests__/stringify.spec.ts b/nodepkg/jsoncue/src/__tests__/stringify.spec.ts new file mode 100644 index 00000000..671143b8 --- /dev/null +++ b/nodepkg/jsoncue/src/__tests__/stringify.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "bun:test"; +import { isValidIdentity } from "../astutil"; + +describe("#isValidIdentity", () => { + it("identity should true", () => { + for (const id of [ + "job", + "job1" + ]) { + expect(isValidIdentity(id)).toBeTrue(); + } + }); + + it("invalid identity should false", () => { + for (const id of [ + "job-1", + "1" + ]) { + expect(isValidIdentity(id)).toBeFalsy(); + } + }); +}); \ No newline at end of file diff --git a/nodepkg/jsoncue/src/astutil/stringify.ts b/nodepkg/jsoncue/src/astutil/stringify.ts index 22084637..aab7dfac 100644 --- a/nodepkg/jsoncue/src/astutil/stringify.ts +++ b/nodepkg/jsoncue/src/astutil/stringify.ts @@ -164,8 +164,8 @@ function isValidJSONValue(v: any) { return !(isUndefined(v) || isFunction(v)); } -function isValidIdentity(v: string) { - return /[_$A-Za-z0-9]+/.test(v); +export function isValidIdentity(v: string) { + return /^[A-Za-z$_]([$_A-Za-z0-9]+)?$/.test(v); } function isMultiline(v: string) { diff --git a/nodepkg/jsoncue/src/codemirror/completions.ts b/nodepkg/jsoncue/src/codemirror/completions.ts index c4063817..265f5851 100644 --- a/nodepkg/jsoncue/src/codemirror/completions.ts +++ b/nodepkg/jsoncue/src/codemirror/completions.ts @@ -1,4 +1,4 @@ -import { type AnyType, EmptyContext, t } from "@innoai-tech/typedef"; +import { type AnyType, EmptyContext, SymbolRecordKey, t } from "@innoai-tech/typedef"; import { JSONCue } from "../JSONCue.ts"; import { isArray, isNumber, isObject, isUndefined } from "../astutil/typed.ts"; import type { EditorState } from "@codemirror/state"; @@ -12,7 +12,7 @@ import { selectionAt } from "./util.ts"; import type { SyntaxNode } from "@lezer/common"; import { NodeType } from "../astutil/index.ts"; -function schemaAt(typ: AnyType, values: any, path: any[], ctx = EmptyContext) { +export function schemaAt(typ: AnyType, values: any, path: any[], ctx = EmptyContext) { switch (typ.type) { case "array": if (path.length === 0) { @@ -41,8 +41,38 @@ function schemaAt(typ: AnyType, values: any, path: any[], ctx = EmptyContext) { } break; - case "union": case "record": + if (path.length === 0) { + return typ; + } + + if (isUndefined(values)) { + values = {}; + } + + if (!isObject(values)) { + return; + } + + if (Object.keys(values).length == 0 && path.length > 0) { + (values as any)[path[0]] = undefined; + } + + for (const [key, _, propType] of typ.entries(values, ctx)) { + if (key == SymbolRecordKey) { + continue; + } + + const childValue = (values as any)[key]; + + return schemaAt(propType, childValue, path.slice(1), { + path: [...ctx.path, String(key)], + branch: [...ctx.branch, childValue] + }); + } + + return typ; + case "union": case "object": if (isUndefined(values)) { values = {}; @@ -127,8 +157,9 @@ function asCompletions(typ: AnyType, node: SyntaxNode): Completion[] { ); break; - case "union": case "record": + break; + case "union": case "object": for (const [key, _, propType] of typ.entries({}, EmptyContext)) { const propName = String(key); diff --git a/nodepkg/typedef/package.json b/nodepkg/typedef/package.json index 51ad904e..ae2347f0 100644 --- a/nodepkg/typedef/package.json +++ b/nodepkg/typedef/package.json @@ -1,6 +1,6 @@ { "name": "@innoai-tech/typedef", - "version": "0.2.26", + "version": "0.2.27", "monobundle": { "exports": { ".": "./src/index.ts" diff --git a/nodepkg/typedef/src/encoding/JSONSchemaDecoder.ts b/nodepkg/typedef/src/encoding/JSONSchemaDecoder.ts index 85435bc6..21917bad 100644 --- a/nodepkg/typedef/src/encoding/JSONSchemaDecoder.ts +++ b/nodepkg/typedef/src/encoding/JSONSchemaDecoder.ts @@ -307,7 +307,7 @@ const isMetaType = (schema: any): any => { return !hasProps(schema, ["type", "$ref", "$id", "oneOf", "anyOf", "allOf"]); }; -const normalizeSchema = (schema: any): any => { +const normalizeSchema = (schema: any = {}): any => { if (isBoolean(schema)) { return {}; } diff --git a/webapp/openapi-playground/mod/openapi/RequestBuilder.tsx b/webapp/openapi-playground/mod/openapi/RequestBuilder.tsx index 56e4e923..080fea42 100644 --- a/webapp/openapi-playground/mod/openapi/RequestBuilder.tsx +++ b/webapp/openapi-playground/mod/openapi/RequestBuilder.tsx @@ -6,11 +6,10 @@ import { rx, subscribeOnMountedUntilUnmount, subscribeUntilUnmount, t, - Type, - TypeWrapper, useRoute, + useRoute, useRouter } from "@innoai-tech/vuekit"; -import type { JSONSchema, Operation } from "./models"; +import type { Operation } from "./models"; import { FormData, f, type Field } from "@innoai-tech/vueformdata"; import { TextField } from "./components/TextField"; import { isUndefined } from "./util/typed.ts"; @@ -26,16 +25,6 @@ import { ResponsePreview } from "./ResponsePreview.tsx"; import { HttpRequest } from "./HTTPViews.tsx"; import { mdiUploadBox } from "@mdi/js"; -export function rawSchema(rawSchema: JSONSchema) { - return (t: Type) => { - return TypeWrapper.of(t, { - $meta: { - rawSchema: rawSchema - } - }); - }; -} - export const RequestBuilder = component$({ operation: t.custom(), $default: t.custom() @@ -48,8 +37,7 @@ export const RequestBuilder = component$({ let x: AnyType = JSONSchemaDecoder.decode(p.schema, (ref) => { return [openapi$.schema(ref) ?? {}, refName(ref)]; }).use( - f.label(`${p.name}, in=${JSON.stringify(p.in)}`), - rawSchema(p.schema), + f.label(`${p.name}, in=${JSON.stringify(p.in)}`) ); if (!p.required) { @@ -72,8 +60,7 @@ export const RequestBuilder = component$({ const x = JSONSchemaDecoder.decode(content.schema ?? {}, (ref) => { return [openapi$.schema(ref) ?? {}, refName(ref)]; }).use( - f.label(`body, content-type = ${JSON.stringify(contentType)}`), - rawSchema(content.schema), + f.label(`body, content-type = ${JSON.stringify(contentType)}`) ); if (contentType.includes("json")) { @@ -209,8 +196,6 @@ const ParameterInput = component$( return rx( combineLatest([field$, field$.input$]), render(([s, value]) => { - let rawSchema = field$.meta?.["rawSchema"] as JSONSchema ?? {}; - let Input: any = field$.meta?.input ?? TextInput; const readOnly = (field$.meta?.readOnlyWhenInitialExist ?? false) && !!s.initial; @@ -228,8 +213,8 @@ const ParameterInput = component$( } $supporting={ - - + + } $trailing={(Input as any).$trailing} diff --git a/webapp/openapi-playground/mod/openapi/ResponseView.tsx b/webapp/openapi-playground/mod/openapi/ResponseView.tsx index 43a7144f..b0390767 100644 --- a/webapp/openapi-playground/mod/openapi/ResponseView.tsx +++ b/webapp/openapi-playground/mod/openapi/ResponseView.tsx @@ -1,8 +1,9 @@ -import { component$, t } from "@innoai-tech/vuekit"; +import { component$, JSONSchemaDecoder, refName, t } from "@innoai-tech/vuekit"; import type { Response } from "./models"; import { Box, styled } from "@innoai-tech/vueuikit"; import { Line, PropName, SchemaView, Indent, Token } from "./SchemaView.tsx"; import { isUndefined } from "./util/typed.ts"; +import { OpenAPIProvider } from "./OpenAPIProvider.tsx"; function isErrorCode(c: number | string) { try { @@ -16,6 +17,8 @@ export const ResponseView = component$({ code: t.custom(), response: t.custom() }, (props) => { + const openapi$ = OpenAPIProvider.use(); + return () => { return ( @@ -63,7 +66,15 @@ export const ResponseView = component$({ {Object.entries(props.response.content ?? {}).map(([contentType, { schema }]) => ( - + { + return [ + openapi$.schema(ref) ?? {}, + refName(ref) + ]; + })} + /> +
{contentType} diff --git a/webapp/openapi-playground/mod/openapi/SchemaView.tsx b/webapp/openapi-playground/mod/openapi/SchemaView.tsx index 758ef14c..87ada09b 100644 --- a/webapp/openapi-playground/mod/openapi/SchemaView.tsx +++ b/webapp/openapi-playground/mod/openapi/SchemaView.tsx @@ -1,8 +1,13 @@ -import { component, component$, createProvider, render, rx, t, type VNodeChild } from "@innoai-tech/vuekit"; +import { + type AnyType, + component, + component$, + createProvider, + t, + type VNodeChild +} from "@innoai-tech/vuekit"; +import { isUndefined } from "@innoai-tech/lodash"; import { styled } from "@innoai-tech/vueuikit"; -import type { JSONSchema } from "./models"; -import { OpenAPIProvider } from "./OpenAPIProvider.tsx"; -import { isUndefined } from "./util/typed.ts"; import { Markdown } from "@innoai-tech/vuemarkdown"; export const Token = styled("div")({ @@ -69,13 +74,11 @@ export const Line = styled( export const Description = styled( "div", { - schema: t.custom() + schema: t.custom() }, (props, {}) => { return (Root) => { - const schema = props.schema; - - const description = schema["description"] ?? ""; + const description = props.schema.getMeta("description") ?? ""; if (description.length == 0) { return null; @@ -181,7 +184,7 @@ export const Indent = component( const SchemaViewLink = component$( { - schema: t.custom() + schema: t.custom() }, (props) => { return () => { @@ -198,177 +201,158 @@ const SchemaViewLink = component$( export const SchemaView = component$( { - schema: t.custom() + schema: t.custom() }, (props) => { - const openapi$ = OpenAPIProvider.use(); - const schema = props.schema ?? {}; - - if (schema["$ref"]) { - const ref = schema["$ref"]; - - return rx( - openapi$.schema$(ref), - render((schema) => { - if (schema && schema["$id"]) { - return ( - - ); - } - return null; - }) - ); + const schema = props.schema; + if (schema.getSchema("$ref")) { + return ( + + ); } return () => { - if (Array.isArray(schema["oneOf"])) { - if (schema["discriminator"]) { + switch (schema.type) { + case "union": return ( <> - {Object.entries(schema["discriminator"]["mapping"] ?? {}) - .toSorted() - .map(([value, mappingSchema]) => { - if (!mappingSchema) { - return null; - } - - return ( - <> - -   - - - - {"if "} - {schema["discriminator"]?.["propertyName"]} = - {":"} - - - -    - - - - ); - })} - - ); - } - - return ( - <> - {schema["oneOf"].map((s, i) => { - return ( - <> - {i > 0 && ( -  {" | "}  - )} - {":"} - - - ); - })} - - ); - } - - if (Array.isArray(schema["allOf"])) { - return ( - - {schema["allOf"] - .filter((s) => !(Object.keys(s).length)) - .map((s, i) => { + {schema.getSchema("oneOf")?.map((s, i) => { return ( <> - {i > 0 &&  {"&"} } + {i > 0 && ( +  {" | "}  + )} ); })} - - ); - } - - if (schema.type == "array") { - return ( - - {"Array<"} - - {">"} - - ); - } - - if (schema.type == "object") { - return ( - <> - {schema["$id"] && {schema["$id"]} } - {"{"} - - <> - {Object.entries((schema["properties"] ?? {}) as Record).map(([propName, propSchema]) => { - if (!propSchema) { - return null; - } - + + ); + case "intersection": + return ( + + {schema.getSchema("allOf") + ?.filter((s) => !(Object.keys(s).length)) + .map((s, i) => { return ( <> - - - - - {propName} - - {":"} - + {i > 0 && ( + +  {"&"}  - + )} + ); })} - - - {schema["additionalProperties"] && ( - <> - - - {"[K:"}  - - {"]:"}  - - - - - )} - {"}"} - - ); - } + + ); + case "array": + return ( + + {"Array<"} + + {">"} + + ); + case "object": + return ( + <> + {"{"} + + <> + {Object.entries((schema.getSchema("properties") ?? {}) as Record).map(([propName, propSchema]) => { + if (!propSchema) { + return null; + } + + return ( + <> + + + + + {propName} + + {":"} + + + + + ); + })} + + + {"}"} + + ); + case "record": + return ( + <> + {"{"} + {schema.getSchema("additionalProperties") && ( + <> + + + {"[K:"}  + + {"]:"}  + + + + + )} + {"}"} + + ); + case "enums": { + const enumValues = schema.getSchema("enum") ?? []; - let [type, format, enumValues, defaultValue] = [ - schema["type"], - schema["format"], - schema["enum"], - schema["default"] - ]; + if (enumValues.length == 1) { + return {JSON.stringify(enumValues[0])}; + } - if (enumValues && enumValues.length == 1) { - return {JSON.stringify(enumValues[0])}; - } + let type: string = "any"; - if (!type && (enumValues && enumValues.length > 0)) { - type = typeof enumValues[0]; + if (enumValues.length > 0) { + type = typeof enumValues[0]; + } + + return ( + <> + {type} + + {enumValues.map((value: any, i) => ( + ("enumLabels")?.[i] ? { + "label": JSON.stringify(schema.getMeta("enumLabels")![i]) + } : {}} + /> + ))} + + + ); + } } + let [type, format, defaultValue] = [ + schema.type, + schema.getSchema("format"), + schema.getSchema("default") + ]; + return ( <> {type || "any"} @@ -380,20 +364,6 @@ export const SchemaView = component$( {!hasValidate(schema) && ( )} - {Array.isArray(enumValues) && ( - <> - {enumValues.map((value: any, i) => ( - - ))} - - )} ); @@ -402,7 +372,7 @@ export const SchemaView = component$( ); -function hasValidate(schema: any) { +function hasValidate(schema: AnyType) { return ([ "enum", "maximum", @@ -416,7 +386,7 @@ function hasValidate(schema: any) { "minItems", "maxProperties", "minProperties" - ] as Array).some((key) => Object.hasOwn(schema, key)); + ] as Array).some((key) => schema.getSchema(key)); } @@ -435,48 +405,48 @@ export interface ValidatedSchemaProps { minProperties?: number; } -export function getMax(schema: JSONSchema): string { - if (schema["maxProperties"]) { - return schema["maxProperties"]; +export function getMax(schema: AnyType): string { + if (schema.getSchema("maxProperties")) { + return schema.getSchema("maxProperties")!; } - if (schema["maxItems"]) { - return schema["maxItems"]; + if (schema.getSchema("maxItems")) { + return schema.getSchema("maxItems")!; } - if (schema["maximum"]) { - return schema["maximum"]; + if (schema.getSchema("maximum")) { + return schema.getSchema("maximum")!; } - if (schema["maxLength"]) { - return schema["maxLength"]; + if (schema.getSchema("maxLength")) { + return schema.getSchema("maxLength")!; } - if (schema.type === "string" && schema["format"] === "uint64") { + if (schema.type === "string" && schema.getSchema("format") === "uint64") { return "19"; } if ( (schema.type === "number" || schema.type === "integer") && - schema["format"] + schema.getSchema("format") ) { return `${ - Math.pow(2, Number(schema["format"].replace(/[^0-9]/g, "")) - 1) - 1 + Math.pow(2, Number(schema.getSchema("format").replace(/[^0-9]/g, "")) - 1) - 1 }`; } return "+∞"; } -export function getMin(schema: JSONSchema = {}): string { - if (schema["minProperties"]) { - return schema["minProperties"]; +export function getMin(schema: AnyType): string { + if (schema.getSchema("minProperties")) { + return schema.getSchema("minProperties")!; } - if (schema["minItems"]) { - return schema["minItems"]; + if (schema.getSchema("minItems")) { + return schema.getSchema("minItems")!; } - if (schema["minimum"]) { - return schema["minimum"]; + if (schema.getSchema("minimum")) { + return schema.getSchema("minimum")!; } - if (schema["minLength"]) { - return schema["minLength"]; + if (schema.getSchema("minLength")) { + return schema.getSchema("minLength")!; } if (schema.type === "string") { @@ -484,27 +454,27 @@ export function getMin(schema: JSONSchema = {}): string { } if ( - (schema.type === "number" || schema.type === "integer") && schema["format"] + (schema.type === "number" || schema.type === "integer") && schema.getSchema("format") ) { return `${ - Math.pow(2, Number(schema["format"].replace(/[^0-9]/g, "")) - 1) - 1 + Math.pow(2, Number(schema.getSchema("format").replace(/[^0-9]/g, "")) - 1) - 1 }`; } return "-∞"; } -export function displayValidate(schema: JSONSchema = {}): string { - if (schema["x-tag-validate"]) { - return schema["x-tag-validate"]; +export function displayValidate(schema: AnyType): string { + if (schema.getSchema("x-tag-validate")) { + return schema.getSchema("x-tag-validate")!; } if (!hasValidate(schema)) { return ""; } - if (schema["pattern"]) { - return `@r/${String(schema["pattern"])}/`; + if (schema.getSchema("pattern")) { + return `@r/${String(schema.getSchema("pattern"))}/`; } - return `@${schema["exclusiveMinimum"]} ? "(" : "["}${getMin(schema)},${getMax(schema)}${schema["exclusiveMaximum"] ? ")" : "]"}`; + return `@${schema.getSchema("exclusiveMinimum")} ? "(" : "["}${getMin(schema)},${getMax(schema)}${schema.getSchema("exclusiveMaximum") ? ")" : "]"}`; }