diff --git a/web/docusaurus.config.ts b/web/docusaurus.config.ts index 2ffae62e..5cec5cfc 100644 --- a/web/docusaurus.config.ts +++ b/web/docusaurus.config.ts @@ -45,7 +45,7 @@ const config: Config = { rules: [ { test: /\.yaml$/, - use: "yaml-loader" + use: "raw-loader" } ] } diff --git a/web/package.json b/web/package.json index cae7a5e0..23f5b004 100644 --- a/web/package.json +++ b/web/package.json @@ -30,13 +30,13 @@ "react-dom": "^18.0.0", "react-markdown": "^9.0.1", "yaml": "^2.3.4", - "yaml-loader": "^0.8.0", "yaml-template": "^1.0.0" }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.0.1", "@docusaurus/tsconfig": "^3.0.1", "@docusaurus/types": "^3.0.1", + "raw-loader": "^4.0.2", "typescript": "~5.2.2" }, "browserslist": { diff --git a/web/spec/type/base.mdx b/web/spec/type/base.mdx index 50fb6272..a1d11ce3 100644 --- a/web/spec/type/base.mdx +++ b/web/spec/type/base.mdx @@ -141,28 +141,28 @@ A type reference is an object containing the single `"id"` field. This field must be a string or a number. + schema={{ id: "schema:ethdebug/format/type/base" }} + pointer="#/$defs/TypeReference" /> #### Type wrapper schema - typeof item === "object" && - !("$schema" in item) && - item["$ref"] === "#/$defs/Type"} - transform={ - ({ $ref, ...rest }, root) => ({ - ...rest, - type: "object", - title: root.$defs.Type.title + " [RECURSIVE]", - description: "The root Type schema" - }) - } - /> + schema={{ id: "schema:ethdebug/format/type/base" }} + pointer="#/$defs/TypeWrapper" + detect={ + (item) => + typeof item === "object" && + !("$schema" in item) && + item["$ref"] === "#/$defs/Type"} + transform={ + ({ $ref, ...rest }, root) => ({ + ...rest, + type: "object", + title: root.$defs.Type.title + " [RECURSIVE]", + description: "The root Type schema" + }) + } + /> ### ComplexType's `"contains"` field @@ -175,99 +175,95 @@ As described [above](#type-wrappers-and-type-references), complex types compose other types. This composition occurs inside the `"contains"` field for all complex types. -#### Example complex types to show different forms +
+ Example complex types to show different forms + + + This is an example array type, which composes exactly one other type. - - - This is an example array type, which composes exactly one other type. - - ```json - { - "kind": "array", - "contains": { - "type": { - "kind": "uint", - "bits": 256 - } - } - } - ``` - - - This is an example array type, which composes an ordered list of member - types. - - ```json - { - "kind": "struct", - "contains": [{ - "member": "balance", - "type": { - "kind": "uint", - "bits": 256 - } - }, { - "member": "scoreSheet", - "type": { - "id": "" - } - }] - } - ``` - - In this example, please note how this struct type represents member names - with a `"member"` field alongside the `"type"` field, and note how the - value of `"type"` can be either a complete representation or a reference - object in the form of `{ id }`. - - - This is an example mapping type, which composes an object mapping of types - by key. - ```json - { - "kind": "mapping", - "contains": { - "key": { + ```json + { + "kind": "array", + "contains": { "type": { - "kind": "address" + "kind": "uint", + "bits": 256 } - }, - "value": { + } + } + ``` + + + This is an example array type, which composes an ordered list of member + types. + + ```json + { + "kind": "struct", + "contains": [{ + "member": "balance", "type": { "kind": "uint", "bits": 256 } + }, { + "member": "scoreSheet", + "type": { + "id": "" + } + }] + } + ``` + + In this example, please note how this struct type represents member names + with a `"member"` field alongside the `"type"` field, and note how the + value of `"type"` can be either a complete representation or a reference + object in the form of `{ id }`. + + + This is an example mapping type, which composes an object mapping of types + by key. + ```json + { + "kind": "mapping", + "contains": { + "key": { + "type": { + "kind": "address" + } + }, + "value": { + "type": { + "kind": "uint", + "bits": 256 + } + } } } - } - ``` - - + ``` + + +
## Full base schema - typeof item === "object" && - !("$schema" in item) && - item["$ref"] === "schema:ethdebug/format/type/base"} - transform={ - ({ $ref, ...rest }, root) => ({ - ...rest, - type: "object", - title: root.title + " [RECURSIVE]", - description: "The root Type schema" - }) - } - /> + schema={{ id: "schema:ethdebug/format/type/base" }} + detect={ + (item) => + typeof item === "object" && + !("$schema" in item) && + item["$ref"] === "schema:ethdebug/format/type/base"} + transform={ + ({ $ref, ...rest }, root) => ({ + ...rest, + type: "object", + title: root.title + " [RECURSIVE]", + description: "The root Type schema" + }) + } + /> + ## Example schema extensions for particular types These examples show valid schemas that extend **ethdebug/format/types/base** diff --git a/web/spec/type/type.mdx b/web/spec/type/type.mdx index a3a7d210..ccab15d8 100644 --- a/web/spec/type/type.mdx +++ b/web/spec/type/type.mdx @@ -4,7 +4,6 @@ sidebar_position: 2 import SchemaViewer from "@site/src/components/SchemaViewer"; import { Collapsible, CreateTypes } from "@theme/JSONSchemaViewer/components"; -import { schemas } from "@site/src/loadSchema"; # ethdebug/format/type [placeholder] diff --git a/web/src/components/SchemaListing.tsx b/web/src/components/SchemaListing.tsx new file mode 100644 index 00000000..b76ae1a3 --- /dev/null +++ b/web/src/components/SchemaListing.tsx @@ -0,0 +1,66 @@ +import YAML from "yaml"; +import { + type DescribeSchemaOptions, + describeSchema +} from "@site/src/schemas"; +import CodeBlock from "@theme/CodeBlock"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +export interface SchemaListingProps extends DescribeSchemaOptions { +} + +export default function SchemaListing(props: SchemaListingProps): JSX.Element { + const { + id: qualifiedId, + pointer, + schema, + yaml + } = describeSchema(props); + + const id = + qualifiedId + ? qualifiedId.startsWith("schema:") + ? qualifiedId.slice(7) + : qualifiedId + : undefined; + + const reference = id && pointer + ? `${id}${pointer}` + : id + ? id + : undefined; + + return ( + + + { + yaml + } + + + { + JSON.stringify(schema, undefined, 2) + } + + + ); +} diff --git a/web/src/components/SchemaViewer.tsx b/web/src/components/SchemaViewer.tsx index 08cbc7ef..12dc20ff 100644 --- a/web/src/components/SchemaViewer.tsx +++ b/web/src/components/SchemaViewer.tsx @@ -1,16 +1,19 @@ import JSONSchemaViewer from "@theme/JSONSchemaViewer"; import CodeBlock from "@theme/CodeBlock"; -import { loadSchema } from "@site/src/loadSchema"; +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; +import { + type DescribeSchemaOptions, + describeSchema +} from "@site/src/schemas"; import ReactMarkdown from "react-markdown"; +import SchemaListing from "./SchemaListing"; -export interface SchemaViewerProps { - schema: string | object; - pointer?: string; +export interface SchemaViewerProps extends DescribeSchemaOptions { detect?: (item: object) => boolean; transform?: (item: object, root: object) => object; } - export const transformObject = ( obj: object, predicate: (item: object) => boolean, @@ -39,58 +42,70 @@ export const transformObject = ( return process(obj); } -export default function SchemaViewer({ - schema: schemaName, - pointer = "", - detect = () => false, - transform = (x) => x -}: SchemaViewerProps): JSX.Element { - const rawSchema = typeof schemaName === "string" - ? loadSchema(schemaName) - : schemaName; - const schema = transformObject( +export default function SchemaViewer(props: SchemaViewerProps): JSX.Element { + const { + schema: rawSchema, + yaml, + pointer + } = describeSchema(props); + + const { + detect = () => false, + transform = (x) => x + } = props; + + const transformedSchema = transformObject( rawSchema, detect, transform ); return ( - { - const schema = loadSchema(uri.toString()); - return schema; - + + + { + const { schema } = describeSchema({ + schema: { + id: uri.toString() + } + }); + return schema; + } + } } - } - } - }} - viewerOptions={{ - showExamples: true, - ValueComponent: ({ value }) => { - // deal with simple types first - if ([ - "string", - "number", - "bigint", - "boolean" - ].includes(typeof value)) { - return { - (value as string | number | bigint | boolean).toString() - }; - } + }} + viewerOptions={{ + showExamples: true, + ValueComponent: ({ value }) => { + // deal with simple types first + if ([ + "string", + "number", + "bigint", + "boolean" + ].includes(typeof value)) { + return { + (value as string | number | bigint | boolean).toString() + }; + } - // for complex types use a whole CodeBlock - return {`${ - JSON.stringify(value, undefined, 2) - }`}; - }, - DescriptionComponent: ({description}) => - - }} /> + // for complex types use a whole CodeBlock + return {`${ + JSON.stringify(value, undefined, 2) + }`}; + }, + DescriptionComponent: ({description}) => + + }} /> + + + + + ); } diff --git a/web/src/css/custom.css b/web/src/css/custom.css index ab6677e9..f8ab5609 100644 --- a/web/src/css/custom.css +++ b/web/src/css/custom.css @@ -29,6 +29,11 @@ --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } +.schema-listing pre { + max-height: 500px; + overflow: auto; +} + .navbar .draft-warning { font-weight: bold; text-decoration: underline; diff --git a/web/src/loadSchema.ts b/web/src/loadSchema.ts deleted file mode 100644 index 7dcff92f..00000000 --- a/web/src/loadSchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import typeBaseSchemaYaml from "../../schemas/type/base.schema.yaml"; - - -export const schemas = [ - typeBaseSchemaYaml, -].map(schema => ({ - [schema.$id]: schema -})).reduce((a, b) => ({ ...a, ...b }), {}); - -export const loadSchema = (id) => { - const schema = schemas[id]; - return schema; -} diff --git a/web/src/schemas.ts b/web/src/schemas.ts new file mode 100644 index 00000000..f22bb04b --- /dev/null +++ b/web/src/schemas.ts @@ -0,0 +1,194 @@ +import YAML from "yaml"; + +import typeBaseSchemaYaml from "../../schemas/type/base.schema.yaml"; + +export const schemaYamls = [ + typeBaseSchemaYaml, +].map(schema => ({ + [YAML.parse(schema).$id]: schema +})).reduce((a, b) => ({ ...a, ...b }), {}); + +export interface DescribeSchemaOptions< + S extends SchemaReference = SchemaReference +> { + schema: S; + pointer?: SchemaPointer; +}; + +export interface DescribedSchema { + id?: string; // root ID only + pointer: SchemaPointer; // normalized from root ID + schema: object; + yaml: string; +} + +export function describeSchema({ + schema, + pointer +}: DescribeSchemaOptions): DescribedSchema { + if (typeof pointer === "string" && !pointer.startsWith("#")) { + throw new Error("`pointer` option must start with '#'"); + } + + return referencesId(schema) + ? describeSchemaById({ schema, pointer }) + : referencesYaml(schema) + ? describeSchemaByYaml({ schema, pointer }) + : describeSchemaByObject({ schema, pointer }); +} + +function describeSchemaById({ + schema: { id: referencedId }, + pointer: relativePointer +}: DescribeSchemaOptions): DescribedSchema { + // we need to handle the case where the schema is referenced by an ID + // with a pointer specified, possibly with a separate `pointer` field too + const [id, rawReferencedPointer] = referencedId.split("#"); + + const pointer = rawReferencedPointer + ? joinSchemaPointers([`#${rawReferencedPointer}`, relativePointer]) + : relativePointer; + + const rootYaml = schemaYamls[id] + if (!rootYaml) { + throw new Error(`Unknown schema with $id "${id}"`); + } + + const yaml = pointToYaml(rootYaml, pointer); + + const schema = YAML.parse(yaml); + + return { + id, + pointer, + yaml, + schema + } +} + +function describeSchemaByYaml({ + schema: { yaml: referencedYaml }, + pointer +}: DescribeSchemaOptions): DescribedSchema { + const yaml = pointToYaml(referencedYaml, pointer); + + const schema = YAML.parse(yaml); + + const id = schema.$id; + + if (id) { + return { + id, + pointer, + yaml, + schema + } + } else { + return { + pointer, + yaml, + schema + } + } +} + +function describeSchemaByObject({ + schema: referencedSchema, + pointer +}: DescribeSchemaOptions): DescribedSchema { + const rootYaml = YAML.stringify(referencedSchema); + + const yaml = pointToYaml(rootYaml, pointer); + + const schema = YAML.parse(yaml); + + const id = schema.$id; + + if (id) { + return { + id, + pointer, + yaml, + schema + } + } else { + return { + pointer, + yaml, + schema + } + } +} + +function joinSchemaPointers( + pointers: (SchemaPointer | undefined)[] +): SchemaPointer | undefined { + const joined = pointers + .filter((pointer): pointer is SchemaPointer => typeof pointer === "string") + .map(pointer => pointer.slice(1)) + .join(""); + + if (joined.length === 0) { + return; + } + + return `#${joined}`; +} + +function pointToYaml( + yaml: string, + pointer?: SchemaPointer +): string { + if (!pointer) { + return yaml; + } + + let doc = YAML.parseDocument(yaml); + + // slice(2) because we want to remove leading #/ + for (const step of pointer.slice(2).split("/")) { + // @ts-ignore + doc = doc.get(step, true); + + if (!doc) { + throw new Error(`Pointer ${pointer} not found in schema`); + } + } + + return YAML.stringify(doc); +} + +type Impossible = { + [P in K]: never; +}; + +type NoExtraProperties = + & U + & Impossible>; + +export type SchemaPointer = `#${string}`; + +export type SchemaReference = + | SchemaById + | SchemaByYaml + | object /* JSONSchema object itself */; + +export type SchemaById = NoExtraProperties<{ + id: string; +}>; + +export type SchemaByYaml = NoExtraProperties<{ + yaml: string; +}>; + +export function referencesId( + schema: SchemaReference +): schema is SchemaById { + return Object.keys(schema).length === 1 && "id" in schema; +} + +export function referencesYaml( + schema: SchemaReference +): schema is SchemaByYaml { + return Object.keys(schema).length === 1 && "yaml" in schema; +} diff --git a/web/types.d.ts b/web/types.d.ts index 63ad776c..4bc1894c 100644 --- a/web/types.d.ts +++ b/web/types.d.ts @@ -1,4 +1,4 @@ declare module "*.yaml" { - const content: { [key: string]: any }; + const content: string; export default content; } diff --git a/web/yarn.lock b/web/yarn.lock index 58a27609..ee099ca4 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -5154,11 +5154,6 @@ isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== -javascript-stringify@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" - integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== - jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" @@ -7069,6 +7064,14 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-loader@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-4.0.2.tgz#1aac6b7d1ad1501e66efdac1522c73e59a584eb6" + integrity sha512-ZnScIV3ag9A4wPX/ZayxL/jZH+euYb6FcUinPcgiQW0+UBtEv0O6Q3lGd3cqJ+GHH+rksEv3Pj99oxJ3u3VIKA== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + rc@1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -8612,15 +8615,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml-loader@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/yaml-loader/-/yaml-loader-0.8.0.tgz#c839325e3fdee082b3768b2a21fe34fde5d96f61" - integrity sha512-LjeKnTzVBKWiQBeE2L9ssl6WprqaUIxCSNs5tle8PaDydgu3wVFXTbMfsvF2MSErpy9TDVa092n4q6adYwJaWg== - dependencies: - javascript-stringify "^2.0.1" - loader-utils "^2.0.0" - yaml "^2.0.0" - yaml-template@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yaml-template/-/yaml-template-1.0.0.tgz#e32e8d4077e67bbfae1e803e0b34e9f2b9298ceb" @@ -8633,7 +8627,7 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== -yaml@^2.0.0, yaml@^2.3.4: +yaml@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.4.tgz#53fc1d514be80aabf386dc6001eb29bf3b7523b2" integrity sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==