Skip to content

Commit

Permalink
Merge pull request #56 from ethdebug/customize-schema-components
Browse files Browse the repository at this point in the history
Customize schema component rendering
  • Loading branch information
gnidan committed Jan 6, 2024
2 parents 044a39a + a69ac3f commit 6de76da
Show file tree
Hide file tree
Showing 8 changed files with 377 additions and 107 deletions.
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"@fortawesome/react-fontawesome": "^0.2.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^1.2.1",
"docusaurus-json-schema-plugin": "^1.9.0",
"docusaurus-json-schema-plugin": "^1.10.0",
"jsonpointer": "^5.0.1",
"prism-react-renderer": "^2.1.0",
"react": "^18.0.0",
Expand Down
149 changes: 70 additions & 79 deletions web/src/components/SchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,108 +4,99 @@ import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";
import {
type DescribeSchemaOptions,
describeSchema
describeSchema,
schemaIndex,
} from "@site/src/schemas";
import {
SchemaContext ,
type PointerSchemaIds,
internalIdKey,
} from "@site/src/contexts/SchemaContext";
import ReactMarkdown from "react-markdown";
import SchemaListing from "./SchemaListing";

export interface SchemaViewerProps extends DescribeSchemaOptions {
detect?: (item: object) => boolean;
transform?: (item: object, root: object) => object;
}

export const transformObject = (
obj: object,
predicate: (item: object) => boolean,
transform: (item: object, root: object) => object
): object => {
const process = (currentObj: object): object => {
if (predicate(currentObj)) {
return transform(currentObj, obj);
}

if (typeof currentObj !== 'object' || currentObj === null) {
return currentObj;
}

// Using Array.isArray to differentiate between array and object
if (Array.isArray(currentObj)) {
return currentObj.map(item => process(item));
} else {
return Object.keys(currentObj).reduce((acc, key) => {
acc[key] = process(currentObj[key]);
return acc;
}, {});
}
};

return process(obj);
}

export default function SchemaViewer(props: SchemaViewerProps): JSX.Element {
const rootSchemaInfo = describeSchema(props);
const {
schema: rawSchema,
id,
rootSchema,
yaml,
pointer
} = describeSchema(props);
} = rootSchemaInfo;

const {
detect = () => false,
transform = (x) => x
} = props;

const transformedSchema = transformObject(
rawSchema,
detect,
transform
);
const transformedSchema = insertIds(rootSchema, `${id}#`);

return (
<Tabs>
<TabItem value="viewer" label="Explore">
<JSONSchemaViewer
schema={transformedSchema}
resolverOptions={{
resolvers: {
schema: {
resolve: (uri) => {
const { schema } = describeSchema({
schema: {
id: uri.toString()
}
});
return schema;
<SchemaContext.Provider value={{
rootSchemaInfo,
schemaIndex,
}}>
<JSONSchemaViewer
schema={transformedSchema}
resolverOptions={{
jsonPointer: pointer,
resolvers: {
schema: {
resolve: (uri) => {
const id = uri.toString();
const { schema } = describeSchema({
schema: { id }
});
return insertIds(schema, `${id}#`);
}
}
}
}
}}
viewerOptions={{
showExamples: true,
ValueComponent: ({ value }) => {
// deal with simple types first
if ([
"string",
"number",
"bigint",
"boolean"
].includes(typeof value)) {
return <code>{
(value as string | number | bigint | boolean).toString()
}</code>;
}
}}
viewerOptions={{
showExamples: true,
ValueComponent: ({ value }) => {
// deal with simple types first
if ([
"string",
"number",
"bigint",
"boolean"
].includes(typeof value)) {
return <code>{
(value as string | number | bigint | boolean).toString()
}</code>;
}

// for complex types use a whole CodeBlock
return <CodeBlock language="json">{`${
JSON.stringify(value, undefined, 2)
}`}</CodeBlock>;
},
DescriptionComponent: ({description}) =>
<ReactMarkdown children={description} />
}} />
// for complex types use a whole CodeBlock
return <CodeBlock language="json">{`${
JSON.stringify(value, undefined, 2)
}`}</CodeBlock>;
},
DescriptionComponent: ({description}) =>
<ReactMarkdown children={description} />
}} />
</SchemaContext.Provider>
</TabItem>
<TabItem value="listing" label="View source">
<SchemaListing schema={props.schema} pointer={props.pointer} />
</TabItem>
</Tabs>
);
}

function insertIds<T>(obj: T, rootId: string): T {
if (Array.isArray(obj)) {
return obj.map((item, index) => insertIds(item, `${rootId}/${index}`)) as T;
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((newObj, key) => {
const value = obj[key];
newObj[key] = insertIds(value, `${rootId}/${key}`);
return newObj;
}, {
[internalIdKey]: rootId.endsWith("#")
? rootId.slice(0, -1)
: rootId
} as T);
}
return obj;
}
19 changes: 19 additions & 0 deletions web/src/contexts/SchemaContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useContext, createContext } from "react";
import type { SchemaInfo, SchemaIndex } from "@site/src/schemas";

export interface SchemaContextValue {
rootSchemaInfo?: SchemaInfo;
schemaIndex: SchemaIndex;
}

export type PointerSchemaIds = {
[jsonPointer: string]: string
};

export const SchemaContext = createContext<SchemaContextValue>({
schemaIndex: {},
});

export const useSchemaContext = () => useContext(SchemaContext);

export const internalIdKey = Symbol("__$internalId");
52 changes: 39 additions & 13 deletions web/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,43 @@ export const schemaYamls = [
[YAML.parse(schema).$id]: schema
})).reduce((a, b) => ({ ...a, ...b }), {});

export type SchemaIndex = {
[schemaId: `schema:${string}`]: {
href: string /* relative or external URL */;
title?: string;
};
};

export const schemaIndex: SchemaIndex = {
"schema:ethdebug/format/type/base": {
title: "ethdebug/format/type/base schema",
href: "/spec/type/base"
},
"schema:ethdebug/format/type/base#/$defs/TypeWrapper": {
title: "Base type wrapper schema",
href: "/spec/type/base#base-type-wrapper-schema",
},
};

export interface DescribeSchemaOptions<
S extends SchemaReference = SchemaReference
> {
schema: S;
pointer?: SchemaPointer;
};

export interface DescribedSchema {
export interface SchemaInfo {
id?: string; // root ID only
pointer: SchemaPointer; // normalized from root ID
schema: object;
yaml: string;
schema: object;
rootSchema: object;
}

export function describeSchema({
schema,
pointer
}: DescribeSchemaOptions): DescribedSchema {
}: DescribeSchemaOptions): SchemaInfo {
if (typeof pointer === "string" && !pointer.startsWith("#")) {
throw new Error("`pointer` option must start with '#'");
}
Expand All @@ -40,7 +59,7 @@ export function describeSchema({
function describeSchemaById({
schema: { id: referencedId },
pointer: relativePointer
}: DescribeSchemaOptions<SchemaById>): DescribedSchema {
}: DescribeSchemaOptions<SchemaById>): SchemaInfo {
// 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("#");
Expand All @@ -57,22 +76,25 @@ function describeSchemaById({
const yaml = pointToYaml(rootYaml, pointer);

const schema = YAML.parse(yaml);
const rootSchema = YAML.parse(rootYaml);

return {
id,
pointer,
yaml,
schema
schema,
rootSchema
}
}

function describeSchemaByYaml({
schema: { yaml: referencedYaml },
pointer
}: DescribeSchemaOptions<SchemaByYaml>): DescribedSchema {
}: DescribeSchemaOptions<SchemaByYaml>): SchemaInfo {
const yaml = pointToYaml(referencedYaml, pointer);

const schema = YAML.parse(yaml);
const rootSchema = YAML.parse(referencedYaml);

const id = schema.$id;

Expand All @@ -81,22 +103,24 @@ function describeSchemaByYaml({
id,
pointer,
yaml,
schema
schema,
rootSchema
}
} else {
return {
pointer,
yaml,
schema
schema,
rootSchema
}
}
}

function describeSchemaByObject({
schema: referencedSchema,
schema: rootSchema,
pointer
}: DescribeSchemaOptions<object>): DescribedSchema {
const rootYaml = YAML.stringify(referencedSchema);
}: DescribeSchemaOptions<object>): SchemaInfo {
const rootYaml = YAML.stringify(rootSchema);

const yaml = pointToYaml(rootYaml, pointer);

Expand All @@ -109,13 +133,15 @@ function describeSchemaByObject({
id,
pointer,
yaml,
schema
schema,
rootSchema
}
} else {
return {
pointer,
yaml,
schema
schema,
rootSchema
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from "react"

import Translate from "@docusaurus/Translate"

import {
IfElseThen,
DependentRequired,
DependentSchemas,
Dependencies,
} from "@theme-original/JSONSchemaViewer/JSONSchemaElements/SchemaConditional"

import { Collapsible } from "@theme-original/JSONSchemaViewer/components";


type Props = {
schema: any;
}

// To handle Schema Conditional (if-then-else , dependentRequired , dependentSchemas , dependencies )
export default function SchemaConditional(props: Props): JSX.Element {
const { schema } = props

// Checks
const isIfThenElse = schema.if !== undefined

const isDependentRequired =
schema.dependentRequired !== undefined
const isDependentSchemas =
schema.dependentSchemas !== undefined
const isDependencies = schema.dependencies !== undefined

return (
<>
{/* Handles if-then-else case */}
{isIfThenElse && <IfElseThen schema={schema} />}
{/* Handles dependentRequired case */}
{isDependentRequired && <DependentRequired schema={schema} />}
{/* Handles dependentSchemas case */}
{isDependentSchemas && <DependentSchemas schema={schema} />}
{/* Handles dependencies (deprecated) */}
{isDependencies && <Dependencies schema={schema} />}
</>
)
}
Loading

0 comments on commit 6de76da

Please sign in to comment.