Skip to content

Commit

Permalink
Merge pull request #75 from ethdebug/convention-cleanup
Browse files Browse the repository at this point in the history
Cleanup schema explorer override code for polymorphic discriminators
  • Loading branch information
gnidan authored Jan 28, 2024
2 parents 9944017 + 6d07c81 commit a3f90bc
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 139 deletions.
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@docusaurus/module-type-aliases": "^3.0.1",
"@docusaurus/tsconfig": "^3.0.1",
"@docusaurus/types": "^3.0.1",
"json-schema-typed": "^8.0.1",
"typescript": "~5.2.2"
},
"browserslist": {
Expand Down
3 changes: 2 additions & 1 deletion packages/web/src/contexts/SchemaContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useContext, createContext } from "react";
import type { SchemaInfo, SchemaIndex } from "@site/src/schemas";
import type { SchemaInfo } from "@ethdebug/format";
import type { SchemaIndex } from "@site/src/schemas";

export interface SchemaContextValue {
rootSchemaInfo?: SchemaInfo;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import React from 'react';
import type { JSONSchema } from "json-schema-typed/draft-2020-12"
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

import {
SchemaHierarchyContextProvider,
useSchemaHierarchyContext,
} from "@theme-original/JSONSchemaViewer/contexts"

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

export interface Discriminator {
propertyName: string;
schemasByConst: {
[value: string]: {
schema: JSONSchema;
index: number;
}
}
}

export interface DiscriminatorSchemaProps extends Discriminator {
}

export default function DiscriminatorSchema({
propertyName,
schemasByConst
}: DiscriminatorSchemaProps): JSX.Element {
const { jsonPointer: currentJsonPointer, level: currentLevel } =
useSchemaHierarchyContext()

return (
<div>
<hr />
<span className="badge badge--info">polymorphic discriminator</span>&nbsp;
The value of the <strong>{propertyName}</strong> field
determines which sub-schema applies:

<Tabs>{
Object.entries(schemasByConst)
.map(([value, { schema, index }]) => (
<TabItem
key={value}
label={value}
value={value}
>
<SchemaHierarchyContextProvider
value={{
level: currentLevel + 1,
jsonPointer: `${currentJsonPointer}/allOf/${index}/then`,
}}
>
<CreateNodes schema={schema} />
</SchemaHierarchyContextProvider>
</TabItem>
))
}</Tabs>
</div>
);
}

export function detectDiscriminator(schema: {
allOf: JSONSchema[]
}): Discriminator | undefined {
const { allOf } = schema;

const allIfThen = allOf.every(
(clause: JSONSchema): clause is { "if": JSONSchema; then: JSONSchema } => {
if (typeof clause === "boolean") {
return false;
}

const { title, description, "if": if_, then, ...others } = clause;

return !!if_ && !!then && Object.keys(others).length === 0;
}
)

if (!allIfThen) {
return;
}

const allIfsHaveSinglePropertyWithConst = allOf.every(
(ifThen: { "if": JSONSchema; then: JSONSchema }): ifThen is {
"if": {
properties: {
[propertyName: string]: {
"const": string
}
}
};
then: JSONSchema;
} => {
const { "if": if_ } = ifThen;

if (
typeof if_ === "boolean" ||
!("properties" in if_) ||
!if_.properties
) {
return false;
}

const ifProperties = if_.properties;

if (Object.keys(ifProperties).length !== 1) {
return false;
}

const propertyName = Object.keys(ifProperties)[0];
const propertySchema = ifProperties[propertyName];

return (
typeof propertySchema === "object" &&
"const" in propertySchema &&
typeof propertySchema.const === "string" &&
!!propertySchema.const
) ;
}
);

if (!allIfsHaveSinglePropertyWithConst) {
return;
}

const propertyName = Object.keys(allOf[0]["if"].properties)[0];

const schemasByConst = allOf
.map(({ "if": if_, then }, index) => {
const value = if_.properties[propertyName]["const"];

return {
[value]: {
schema: then,
index
}
};
})
.reduce((a, b) => ({ ...a, ...b }), {});


const isUniquelyDiscriminating =
Object.keys(schemasByConst).length === allOf.length;

if (!isUniquelyDiscriminating) {
return;
}

return {
propertyName,
schemasByConst
};
}
Original file line number Diff line number Diff line change
@@ -1,147 +1,25 @@
import React from 'react';
import type { JSONSchema } from "json-schema-typed/draft-2020-12"
import AllOfSchema from '@theme-original/JSONSchemaViewer/JSONSchemaElements/schemaComposition/allOfSchema';
import Tabs from "@theme/Tabs";
import TabItem from "@theme/TabItem";

import { CreateNodes } from "@theme-original/JSONSchemaViewer/components"
import {
SchemaHierarchyContextProvider,
useSchemaHierarchyContext,
} from "@theme-original/JSONSchemaViewer/contexts"
import { Collapsible } from "@theme-original/JSONSchemaViewer/components";
import DiscriminatorSchema, {
detectDiscriminator
} from "./DiscriminatorSchema";

export default function allOfSchemaWrapper(props) {
export default function allOfSchemaWrapper(props: {
schema: Exclude<JSONSchema, boolean> & { allOf: JSONSchema[]; }
}) {
const { schema } = props;
const { jsonPointer: currentJsonPointer, level: currentLevel } =
useSchemaHierarchyContext()

const discriminator = detectDiscriminator(schema);
if (!discriminator) {
return (
<>
<AllOfSchema {...props} />
</>
);
if (discriminator) {
return <DiscriminatorSchema {...discriminator} />
}

const { propertyName, schemasByConst } = discriminator;

return (
<div>
<hr />
<span className="badge badge--info">polymorphic discriminator</span>&nbsp;
The value of the <strong>{propertyName}</strong> field
determines which sub-schema applies:

<Tabs>{
Object.entries(schemasByConst)
.map(([value, { schema, index }]) => (
<TabItem
key={value}
label={value}
value={value}
>
<SchemaHierarchyContextProvider
value={{
level: currentLevel + 1,
jsonPointer: `${currentJsonPointer}/allOf/${index}`,
}}
>
<CreateNodes schema={schema} />
</SchemaHierarchyContextProvider>
</TabItem>
))
}</Tabs>
</div>
<>
<AllOfSchema {...props} />
</>
);
}

function detectDiscriminator(schema: {
allOf: any[]
}): {
propertyName: string;
schemasByConst: {
[value: string]: {
schema: object;
index: number;
}
}
} | undefined {
const { allOf } = schema;

const allIfThen = allOf.every(
(clause: any): clause is { "if": any; then: any } => {
const { title, description, "if": if_, then, ...others } = clause;

return if_ && then && Object.keys(others).length === 0;
}
)

if (!allIfThen) {
return;
}

const allIfsHaveSinglePropertyWithConst = allOf.every(
(ifThen: { "if": any; then: any }): ifThen is {
"if": {
properties: {
[propertyName: string]: {
"const": string
}
}
};
then: any;
} => {
const { "if": if_ } = ifThen;

if (!("properties" in if_)) {
return false;
}

const ifProperties = if_.properties;

if (Object.keys(ifProperties).length !== 1) {
return false;
}

const propertyName = Object.keys(ifProperties)[0];

const { "const": const_ } = ifProperties[propertyName];

return typeof const_ === "string" && !!const_;
}
);

if (!allIfsHaveSinglePropertyWithConst) {
return;
}

const propertyName = Object.keys(allOf[0]["if"].properties)[0];

const schemasByConst = allOf
.map(({ "if": if_, then }, index) => {
const value = if_.properties[propertyName]["const"];

return {
[value]: {
schema: then,
index
}
};
})
.reduce((a, b) => ({ ...a, ...b }), {});


const isUniquelyDiscriminating =
Object.keys(schemasByConst).length === allOf.length;

if (!isUniquelyDiscriminating) {
return;
}

return {
propertyName,
schemasByConst
};
}

Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import React from 'react';
import CreateTypes from "@theme-original/JSONSchemaViewer/components/CreateTypes";
import CreateNodes from '@theme-original/JSONSchemaViewer/components/CreateNodes';
import { useJSVOptionsContext, useSchemaHierarchyContext } from "@theme-original/JSONSchemaViewer/contexts";
import type { JSONSchema } from "json-schema-typed/draft-2020-12";
import { useSchemaHierarchyContext } from "@theme-original/JSONSchemaViewer/contexts";
import { useSchemaContext, internalIdKey } from "@site/src/contexts/SchemaContext";
import Link from "@docusaurus/Link";

export default function CreateNodesWrapper(props) {
export default function CreateNodesWrapper(props: {
schema: Exclude<JSONSchema, boolean> & {
[internalIdKey]: string
}
}) {
const { level } = useSchemaHierarchyContext();
const { schemaIndex } = useSchemaContext();

Expand All @@ -25,7 +30,7 @@ export default function CreateNodesWrapper(props) {
? id.slice("schema:".length)
: id
} schema`
} = schemaIndex[id];
} = schemaIndex[id as keyof typeof schemaIndex];

return (
<>
Expand All @@ -36,7 +41,7 @@ export default function CreateNodesWrapper(props) {

return (
<>
<CreateNodes schema={schema} {...otherProps} />
<CreateNodes schema={props.schema} {...otherProps} />
</>
);
}
1 change: 1 addition & 0 deletions packages/web/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"extends": "@docusaurus/tsconfig",
"compilerOptions": {
"baseUrl": ".",
"strict": true,
"resolveJsonModule": true,
// Extending "@tsconfig/docusaurus/tsconfig.json".types with "docusaurus-json-schema-plugin"
"types": ["node", "@docusaurus/module-type-aliases", "@docusaurus/theme-classic", "docusaurus-json-schema-plugin"]
Expand Down

0 comments on commit a3f90bc

Please sign in to comment.