Skip to content

Commit

Permalink
feat: augment schemas with isCycle
Browse files Browse the repository at this point in the history
fixes: #382
  • Loading branch information
BelfordZ committed Jun 17, 2021
1 parent 4a59ff7 commit 3653185
Show file tree
Hide file tree
Showing 10 changed files with 134 additions and 22 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"!build/**/*.test.*"
],
"devDependencies": {
"@json-schema-tools/dereferencer": "^1.5.2",
"@json-schema-tools/meta-schema": "^1.6.10",
"@types/inquirer": "^7.3.1",
"@types/jest": "^26.0.15",
Expand All @@ -48,6 +47,7 @@
"typescript": "4.2.4"
},
"dependencies": {
"@json-schema-tools/dereferencer": "^1.5.2",
"@json-schema-tools/referencer": "^1.0.4",
"@json-schema-tools/titleizer": "^1.0.5",
"@json-schema-tools/traverse": "^1.7.8",
Expand Down
34 changes: 19 additions & 15 deletions src/codegens/rust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,8 @@ export default class Rust extends CodeGen {
isRequired = s.required.indexOf(key) !== -1;
}

const refTitle = this.refToTitle(propSchema);
let typeName = this.getSafeTitle(refTitle);
const isCycle = refTitle === s.title;
if (isCycle) {
let typeName = this.getSafeTitle(this.refToTitle(propSchema));
if (propSchema !== false && propSchema !== true && propSchema.isCycle) {
typeName = `Box<${typeName}>`;
}

Expand Down Expand Up @@ -248,7 +246,7 @@ export default class Rust extends CodeGen {
}

protected handleAnyOf(s: JSONSchemaObject): TypeIntermediateRepresentation {
return this.buildEnum(s.anyOf as JSONSchema[]);
return this.buildEnum(s.anyOf as JSONSchema[], s);
}

/**
Expand All @@ -259,7 +257,7 @@ export default class Rust extends CodeGen {
}

protected handleOneOf(s: JSONSchemaObject): TypeIntermediateRepresentation {
return this.buildEnum(s.oneOf as JSONSchema[]);
return this.buildEnum(s.oneOf as JSONSchema[], s);
}

protected handleConstantBool(s: JSONSchemaBoolean): TypeIntermediateRepresentation {
Expand All @@ -272,21 +270,27 @@ export default class Rust extends CodeGen {
return { documentationComment: this.buildDocs(s), prefix: "type", typing: "serde_json::Value" };
}

private buildEnum(s: JSONSchema[]): TypeIntermediateRepresentation {
private buildEnum(s: JSONSchema[], parentSchema: JSONSchemaObject): TypeIntermediateRepresentation {
const typeLines = s
.map((enumItem) => {
const refTitle = this.refToTitle(enumItem);
let typeName = this.getSafeTitle(refTitle);
let rhsTypeName = typeName;
if (enumItem !== false && enumItem !== true && enumItem.isCycle) {
rhsTypeName = `Box<${typeName}>`;
}
return `${typeName}(${rhsTypeName})`;
})
.map((l) => ` ${l},`)
.join("\n");

return {
macros: [
"#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]",
"#[serde(untagged)]"
].join("\n"),
prefix: "enum",
typing: [
"{",
this.getJoinedSafeTitles(s, "\n")
.split("\n")
.map((l) => ` ${l}(${l}),`)
.join("\n"),
"}",
].join("\n"),
typing: ["{", typeLines, "}"].join("\n"),
imports: [
"use serde::{Serialize, Deserialize};",
]
Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { JSONSchema } from "@json-schema-tools/meta-schema";
import { capitalize, combineSchemas, replaceTypeAsArrayWithOneOf } from "./utils";
import { capitalize, combineSchemas, replaceTypeAsArrayWithOneOf, getCycleMap, setIsCycle } from "./utils";
import titleizer from "@json-schema-tools/titleizer";
import referencer from "@json-schema-tools/referencer";
import { CodeGen } from "./codegens/codegen";
Expand All @@ -21,7 +21,9 @@ export class Transpiler {
const inputSchema: JSONSchema[] = useMerge ? s as JSONSchema[] : [s];
const noTypeArrays = inputSchema.map(replaceTypeAsArrayWithOneOf);
const schemaWithTitles = noTypeArrays.map(titleizer);
const cycleMap = getCycleMap(schemaWithTitles);
const reffed = schemaWithTitles.map(referencer);
const reffedAndCycleMarked = reffed.map((s) => setIsCycle(s, cycleMap));
if (useMerge) {
this.megaSchema = combineSchemas(reffed);
} else {
Expand Down
38 changes: 36 additions & 2 deletions src/integration-tests/expecteds/go/circular-reference.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,37 @@
import "encoding/json"
import "errors"
type StringDoaGddGA string
type OneOfMoebiusSchemaStringDoaGddGANK2MA6NR struct {
MoebiusSchema *MoebiusSchema
StringDoaGddGA *StringDoaGddGA
}
// UnmarshalJSON implements the json Unmarshaler interface.
// This implementation DOES NOT assert that ONE AND ONLY ONE
// of the simple properties is satisfied; it lazily uses the first one that is satisfied.
// Ergo, it will not return an error if more than one property is valid.
func (o *OneOfMoebiusSchemaStringDoaGddGANK2MA6NR) UnmarshalJSON(bytes []byte) error {
var myMoebiusSchema MoebiusSchema
if err := json.Unmarshal(bytes, &myMoebiusSchema); err == nil {
o.MoebiusSchema = &myMoebiusSchema
return nil
}
var myStringDoaGddGA StringDoaGddGA
if err := json.Unmarshal(bytes, &myStringDoaGddGA); err == nil {
o.StringDoaGddGA = &myStringDoaGddGA
return nil
}
return errors.New("failed to unmarshal one of the object properties")
}
func (o OneOfMoebiusSchemaStringDoaGddGANK2MA6NR) MarshalJSON() ([]byte, error) {
if o.MoebiusSchema != nil {
return json.Marshal(o.MoebiusSchema)
}
if o.StringDoaGddGA != nil {
return json.Marshal(o.StringDoaGddGA)
}
return nil, errors.New("failed to marshal any one of the object properties")
}
type MoebiusSchema struct {
MoebiusProperty *MoebiusSchema `json:"MoebiusProperty,omitempty"`
}
MoebiusProperty *MoebiusSchema `json:"MoebiusProperty,omitempty"`
DeeperMobiusProperty *OneOfMoebiusSchemaStringDoaGddGANK2MA6NR `json:"deeperMobiusProperty,omitempty"`
}
7 changes: 7 additions & 0 deletions src/integration-tests/expecteds/py/circular-reference.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
from typing import TypedDict
from typing import Optional
from typing import NewType
from typing import Union

StringDoaGddGA = NewType("StringDoaGddGA", str)

OneOfMoebiusSchemaStringDoaGddGANK2MA6NR = NewType("OneOfMoebiusSchemaStringDoaGddGANK2MA6NR", Union[MoebiusSchema, StringDoaGddGA])

class MoebiusSchema(TypedDict):
MoebiusProperty: Optional[MoebiusSchema]
deeperMobiusProperty: Optional[OneOfMoebiusSchemaStringDoaGddGANK2MA6NR]
9 changes: 9 additions & 0 deletions src/integration-tests/expecteds/rs/circular-reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@ extern crate derive_builder;

use serde::{Serialize, Deserialize};
use derive_builder::Builder;
pub type StringDoaGddGA = String;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(untagged)]
pub enum OneOfMoebiusSchemaStringDoaGddGANK2MA6NR {
MoebiusSchema(Box<MoebiusSchema>),
StringDoaGddGA(StringDoaGddGA),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Builder, Default)]
#[builder(setter(strip_option), default)]
#[serde(default)]
pub struct MoebiusSchema {
#[serde(rename = "MoebiusProperty", skip_serializing_if = "Option::is_none")]
pub moebius_property: Option<Box<MoebiusSchema>>,
#[serde(rename = "deeperMobiusProperty", skip_serializing_if = "Option::is_none")]
pub deeper_mobius_property: Option<OneOfMoebiusSchemaStringDoaGddGANK2MA6NR>,
}
3 changes: 3 additions & 0 deletions src/integration-tests/expecteds/ts/circular-reference.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export type StringDoaGddGA = string;
export type OneOfMoebiusSchemaStringDoaGddGANK2MA6NR = MoebiusSchema | StringDoaGddGA;
export interface MoebiusSchema {
MoebiusProperty?: MoebiusSchema;
deeperMobiusProperty?: OneOfMoebiusSchemaStringDoaGddGANK2MA6NR;
[k: string]: any;
}
4 changes: 2 additions & 2 deletions src/integration-tests/runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ const getTestCaseBase = async (names: string[], languages: string[]): Promise<Te
const testCases: TestCase[] = [];

languages.forEach((language) => {
// if (language !== "ts") { return; }
// if (language !== "rs") { return; }

names.forEach((name) => {
// if (name !== "json-schema-meta-schema") { return; }
// if (name !== "circular-reference") { return; }

promises.push(readFile(`${testCaseDir}/${name}.json`, "utf8")
.then((fileContents) => {
Expand Down
10 changes: 9 additions & 1 deletion src/integration-tests/test-cases/circular-reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
"title": "moebiusSchema",
"type": "object",
"properties": {
"MoebiusProperty": { "$ref": "#" }
"MoebiusProperty": { "$ref": "#" },
"deeperMobiusProperty": {
"oneOf": [
{ "$ref": "#" },
{
"type": "string"
}
]
}
}
}
45 changes: 45 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import deburr from "lodash.deburr";
import trim from "lodash.trim";
import { JSONSchema, Properties, JSONSchemaObject } from "@json-schema-tools/meta-schema";
import traverse from "@json-schema-tools/traverse";
import Dereferencer from "@json-schema-tools/dereferencer";
import referencer from "@json-schema-tools/referencer";

/**
* Capitalize the first letter of the string.
Expand Down Expand Up @@ -169,3 +171,46 @@ export function replaceTypeAsArrayWithOneOf(schema: JSONSchema): JSONSchema {
return subS;
}, { mutable: true });
}

export function getCycleMap(ss: JSONSchema[]): CycleMap {
return ss.reduce((m, s) => {
traverse(s, (subs, isCycle) => {
if (subs === true || subs === false) { return subs; }
if (isCycle) {
m[schemaToRef(subs).$ref] = true;
};
return subs;
}, { mutable: false });

return m;
}, {} as { [k: string]: true });
}

interface CycleMap { [k: string]: true };

const checkCycle = (cycleMap: CycleMap) => (subs: JSONSchema) => {
if (subs !== true && subs !== false && subs.$ref && cycleMap[subs.$ref]) {
subs.isCycle = true;
};
return subs;
};

export function setIsCycle(s: JSONSchema, cycleMap: CycleMap) {
if (s === true || s === false) { return s; }
traverse(s, checkCycle(cycleMap), { mutable: true });

if (s.definitions !== undefined) {
const defs = s.definitions;
Object.keys(defs).forEach((subsKey) => {
if (cycleMap[`#/definitions/${subsKey}`]) {
defs[subsKey].isCycle = true;
}
});
const definitionSchemas = Object.values(defs);

definitionSchemas.forEach((ds) => {
traverse(ds, checkCycle(cycleMap), { mutable: true });
});
}
return s;
}

0 comments on commit 3653185

Please sign in to comment.