Skip to content

Commit

Permalink
feat: this brings schema-utils into parity with
Browse files Browse the repository at this point in the history
the refactoring of the dereferr and meta-schema

additionally the refactor will help support the
allowing custom dereferences from different protocol.
In this pr that functionality is not exposed, but in
subsequent it will be.
  • Loading branch information
zcstarr committed May 29, 2021
1 parent b3e6711 commit e659f42
Show file tree
Hide file tree
Showing 10 changed files with 9,448 additions and 91 deletions.
9,307 changes: 9,293 additions & 14 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@
},
"homepage": "https://github.com/open-rpc/schema-utils-js#readme",
"dependencies": {
"@json-schema-tools/dereferencer": "^1.4.0",
"@json-schema-tools/meta-schema": "^1.5.10",
"@json-schema-tools/reference-resolver": "^1.1.1",
"@open-rpc/meta-schema": "^1.14.0",
"@json-schema-tools/dereferencer": "1.5.1",
"@json-schema-tools/meta-schema": "^1.6.10",
"@json-schema-tools/reference-resolver": "^1.2.1",
"@open-rpc/meta-schema": "1.14.2",
"ajv": "^6.10.0",
"detect-node": "^2.0.4",
"fast-safe-stringify": "^2.0.7",
Expand Down
57 changes: 35 additions & 22 deletions src/dereference-document.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as _fs from "fs-extra";
import dereferenceDocument, { OpenRPCDocumentDereferencingError } from "./dereference-document";
import { OpenrpcDocument, ContentDescriptorObject, JSONSchema } from "@open-rpc/meta-schema";
import defaultResolver from "@json-schema-tools/reference-resolver";
import { OpenrpcDocument, ContentDescriptorObject, JSONSchema, MethodObject } from "@open-rpc/meta-schema";
import { JSONSchemaObject } from "@json-schema-tools/meta-schema";


Expand All @@ -17,7 +18,7 @@ describe("dereferenceDocument", () => {

it("doesnt explode", async () => {
expect.assertions(1);
const document = await dereferenceDocument(workingDocument);
const document = await dereferenceDocument(workingDocument, defaultResolver);
expect(document.methods).toBeDefined();
});

Expand All @@ -32,14 +33,24 @@ describe("dereferenceDocument", () => {
{ name: "abc", params: [], result: { name: "cba", schema: { type: "number" } } }
],
openrpc: "1.0.0-rc1",
});
}, defaultResolver);
expect(document.methods).toBeDefined();
});

it("derefs simple stuff", async () => {
expect.assertions(7);
const testDoc = {
...workingDocument,
"x-methods": {
foobar: {
name: "foobar",
params: [],
result: {
name: "abcfoo",
schema: { type: "number" }
}
}
},
components: {
schemas: {
bigOlBaz: { $ref: "#/components/schemas/bigOlFoo" },
Expand Down Expand Up @@ -91,15 +102,17 @@ describe("dereferenceDocument", () => {
schema: { $ref: "#/components/schemas/bigOlFoo" }
}
});

const document = await dereferenceDocument(testDoc);
expect(document.methods).toBeDefined();
expect(document.methods[0]).toBeDefined();
expect(document.methods[0].params[0]).toBeDefined();
expect((document.methods[0].params[0] as ContentDescriptorObject).name).toBe("bazerino");
expect(document.methods[0].result).toBeDefined();
expect(((document.methods[0].result as ContentDescriptorObject).schema as JSONSchemaObject).title).toBe("bigOlFoo");
expect(((document.methods[0].params as ContentDescriptorObject[])[1].schema as JSONSchemaObject).title).toBe("bigOlFoo");
testDoc.methods.push({"$ref":"#/x-methods/foobar"})

const document = await dereferenceDocument(testDoc, defaultResolver);
const docMethods = document.methods as MethodObject[];
expect(docMethods).toBeDefined();
expect(docMethods[0]).toBeDefined();
expect(docMethods[0].params[0]).toBeDefined();
expect((docMethods[0].params[0] as ContentDescriptorObject).name).toBe("bazerino");
expect(docMethods[0].result).toBeDefined();
expect(((docMethods[0].result as ContentDescriptorObject).schema as JSONSchemaObject).title).toBe("bigOlFoo");
expect(((docMethods[0].params as ContentDescriptorObject[])[1].schema as JSONSchemaObject).title).toBe("bigOlFoo");
});

it("interdependent refs", async () => {
Expand Down Expand Up @@ -145,12 +158,12 @@ describe("dereferenceDocument", () => {
}
} as OpenrpcDocument;

const document = await dereferenceDocument(testDoc);
const document = await dereferenceDocument(testDoc, defaultResolver);
expect(document.methods).toBeDefined();
expect(document.methods[0]).toBeDefined();

const params = document.methods[0].params as ContentDescriptorObject[];
const result = document.methods[0].result as ContentDescriptorObject;
const params = (document.methods[0] as MethodObject).params as ContentDescriptorObject[];
const result = (document.methods[0] as MethodObject).result as ContentDescriptorObject;
expect(params).toBeDefined();
expect(result).toBeDefined();

Expand Down Expand Up @@ -191,7 +204,7 @@ describe("dereferenceDocument", () => {
};

try {
await dereferenceDocument(testDoc as OpenrpcDocument)
await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver)
} catch (e) {
expect(e).toBeInstanceOf(OpenRPCDocumentDereferencingError);
}
Expand All @@ -218,7 +231,7 @@ describe("dereferenceDocument", () => {
};

try {
await dereferenceDocument(testDoc as OpenrpcDocument)
await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver)
} catch (e) {
expect(e).toBeInstanceOf(OpenRPCDocumentDereferencingError);
}
Expand Down Expand Up @@ -248,7 +261,7 @@ describe("dereferenceDocument", () => {
};

try {
await dereferenceDocument(testDoc as OpenrpcDocument)
await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver)
} catch (e) {
expect(e).toBeInstanceOf(OpenRPCDocumentDereferencingError);
}
Expand Down Expand Up @@ -286,7 +299,7 @@ describe("dereferenceDocument", () => {
}
};

const result = await dereferenceDocument(testDoc as OpenrpcDocument) as any;
const result = await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver) as any;

expect(result.methods[0].links[0]).toBe(testDoc.components.links.fooLink)
});
Expand All @@ -312,7 +325,7 @@ describe("dereferenceDocument", () => {
]
};

const result = await dereferenceDocument(testDoc as OpenrpcDocument) as any;
const result = await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver) as any;

expect(result.methods[0].result.schema.type).toBe("string")
});
Expand Down Expand Up @@ -348,7 +361,7 @@ describe("dereferenceDocument", () => {
}
};

const result = await dereferenceDocument(testDoc as OpenrpcDocument) as any;
const result = await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver) as any;

expect(result.methods[0].result.schema.properties.foo).toBe(result.components.schemas.foo);
});
Expand Down Expand Up @@ -386,7 +399,7 @@ describe("dereferenceDocument", () => {
};

try {
await dereferenceDocument(testDoc as OpenrpcDocument) as any;
await dereferenceDocument(testDoc as OpenrpcDocument, defaultResolver) as any;
} catch (e) {
expect(e).toBeInstanceOf(OpenRPCDocumentDereferencingError);
}
Expand Down
42 changes: 25 additions & 17 deletions src/dereference-document.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import Dereferencer from "@json-schema-tools/dereferencer";
import { OpenrpcDocument as OpenRPC, ReferenceObject, ExamplePairingObject, JSONSchema, SchemaComponents, ContentDescriptorComponents, ContentDescriptorObject, OpenrpcDocument, MethodObject } from "@open-rpc/meta-schema";
import { OpenrpcDocument as OpenRPC, ReferenceObject, ExamplePairingObject, JSONSchema, SchemaComponents, ContentDescriptorComponents, ContentDescriptorObject, OpenrpcDocument, MethodObject, MethodOrReference } from "@open-rpc/meta-schema";
import referenceResolver from "@json-schema-tools/reference-resolver";
import safeStringify from "fast-safe-stringify";

export type ReferenceResolver = typeof referenceResolver
/**
* Provides an error interface for OpenRPC Document dereferencing problems
*
Expand All @@ -20,12 +21,13 @@ export class OpenRPCDocumentDereferencingError implements Error {
}
}

const derefItem = async (item: ReferenceObject, doc: OpenRPC) => {
const derefItem = async (item: ReferenceObject, doc: OpenRPC, resolver: ReferenceResolver) => {
const { $ref } = item;
if ($ref === undefined) { return item; }

try {
return await referenceResolver($ref, doc);
// returns resolved value of the reference
return (await resolver.resolve($ref, doc) as any);
} catch (err) {
throw new OpenRPCDocumentDereferencingError([
`unable to eval pointer against OpenRPC Document.`,
Expand All @@ -38,10 +40,10 @@ const derefItem = async (item: ReferenceObject, doc: OpenRPC) => {
}
};

const derefItems = async (items: ReferenceObject[], doc: OpenRPC) => {
const derefItems = async (items: ReferenceObject[], doc: OpenRPC, resolver: ReferenceResolver) => {
const dereffed = [];
for (const i of items) {
dereffed.push(await derefItem(i, doc))
dereffed.push(await derefItem(i, doc, resolver))
}
return dereffed;
};
Expand Down Expand Up @@ -113,29 +115,35 @@ const handleSchemasInsideContentDescriptorComponents = async (doc: OpenrpcDocume
return doc;
};

const handleMethod = async (method: MethodObject, doc: OpenrpcDocument): Promise<MethodObject> => {
const handleMethod = async (methodOrRef: MethodOrReference, doc: OpenrpcDocument, resolver: ReferenceResolver): Promise<MethodObject> => {
let method = methodOrRef as MethodObject;

if(methodOrRef.$ref !== undefined){
method = await derefItem({$ref: methodOrRef.$ref}, doc, resolver)
}

if (method.tags !== undefined) {
method.tags = await derefItems(method.tags as ReferenceObject[], doc);
method.tags = await derefItems(method.tags as ReferenceObject[], doc, resolver);
}

if (method.errors !== undefined) {
method.errors = await derefItems(method.errors as ReferenceObject[], doc);
method.errors = await derefItems(method.errors as ReferenceObject[], doc, resolver);
}

if (method.links !== undefined) {
method.links = await derefItems(method.links as ReferenceObject[], doc);
method.links = await derefItems(method.links as ReferenceObject[], doc, resolver);
}

if (method.examples !== undefined) {
method.examples = await derefItems(method.examples as ReferenceObject[], doc);
method.examples = await derefItems(method.examples as ReferenceObject[], doc, resolver);
for (const exPairing of method.examples as ExamplePairingObject[]) {
exPairing.params = await derefItems(exPairing.params as ReferenceObject[], doc);
exPairing.result = await derefItem(exPairing.result as ReferenceObject, doc);
exPairing.params = await derefItems(exPairing.params as ReferenceObject[], doc, resolver);
exPairing.result = await derefItem(exPairing.result as ReferenceObject, doc, resolver);
}
}

method.params = await derefItems(method.params as ReferenceObject[], doc);
method.result = await derefItem(method.result as ReferenceObject, doc);
method.params = await derefItems(method.params as ReferenceObject[], doc, resolver);
method.result = await derefItem(method.result as ReferenceObject, doc, resolver);


let componentSchemas: SchemaComponents = {};
Expand Down Expand Up @@ -179,14 +187,14 @@ const handleMethod = async (method: MethodObject, doc: OpenrpcDocument): Promise
* ```
*
*/
export default async function dereferenceDocument(openrpcDocument: OpenRPC): Promise<OpenRPC> {
export default async function dereferenceDocument(openrpcDocument: OpenRPC, resolver: ReferenceResolver): Promise<OpenRPC> {
let derefDoc = { ...openrpcDocument };

derefDoc = await handleSchemaComponents(derefDoc);
derefDoc = await handleSchemasInsideContentDescriptorComponents(derefDoc);
const methods = [] as any;
for (const method of derefDoc.methods) {
methods.push(await handleMethod(method, derefDoc));
methods.push(await handleMethod(method, derefDoc, resolver));
}

derefDoc.methods = methods;
Expand Down
16 changes: 13 additions & 3 deletions src/method-call-validator/method-call-validator.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import MethodCallValidator from "./method-call-validator";
import { OpenrpcDocument as OpenRPC, OpenrpcDocument } from "@open-rpc/meta-schema";
import { MethodObject, OpenrpcDocument as OpenRPC, OpenrpcDocument } from "@open-rpc/meta-schema";
import MethodCallParameterValidationError from "./parameter-validation-error";
import MethodCallMethodNotFoundError from "./method-not-found-error";
import MethodNotFoundError from "./method-not-found-error";
import MethodRefUnexpectedError from "./method-ref-unexpected-error";

const getExampleSchema = (): OpenRPC => ({
info: { title: "123", version: "1" },
Expand All @@ -29,9 +30,9 @@ describe("MethodCallValidator", () => {
expect(result).toEqual([]);
});

it("can handle having params undefined", () => {
it("can handle having params empty", () => {
const example = getExampleSchema();
delete example.methods[0].params;
(example.methods[0] as MethodObject).params = [];
const methodCallValidator = new MethodCallValidator(example);
const result = methodCallValidator.validate("foo", ["foobar"]);
expect(result).toEqual([]);
Expand Down Expand Up @@ -144,4 +145,13 @@ describe("MethodCallValidator", () => {
const result0 = methodCallValidator.validate("rawr", { barbar: "123" });
expect(result0).toBeInstanceOf(MethodNotFoundError);
});

it("unexpected reference error when the document has unresolved method reference passed", () => {
const example = getExampleSchema() as any;
example['x-methods']={ foobar: { name: "foobar", params: [],
result: { name: "abcfoo", schema: { type: "number" } } } }
example.methods.push({"$ref":"#/x-methods/foobar"})
expect(()=>new MethodCallValidator(example)).toThrowError(MethodRefUnexpectedError);
});

});
57 changes: 29 additions & 28 deletions src/method-call-validator/method-call-validator.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Ajv, { ErrorObject, Ajv as IAjv } from "ajv";
import { generateMethodParamId } from "../generate-method-id";
import ParameterValidationError from "./parameter-validation-error";
import { OpenrpcDocument as OpenRPC, MethodObject, ContentDescriptorObject } from "@open-rpc/meta-schema";
import { OpenrpcDocument as OpenRPC, MethodObject, ContentDescriptorObject, MethodOrReference } from "@open-rpc/meta-schema";
import MethodNotFoundError from "./method-not-found-error";
import { find, compact } from "../helper-functions";
import MethodRefUnexpectedError from "./method-ref-unexpected-error";

/**
* A class to assist in validating method calls to an OpenRPC-based service. Generated Clients,
Expand All @@ -28,9 +29,13 @@ export default class MethodCallValidator {
constructor(private document: OpenRPC) {
this.ajvValidator = new Ajv();

document.methods.forEach((method: MethodObject) => {
// Validate that the methods are dereferenced
document.methods.forEach((method: MethodOrReference) => {
if (method.$ref) throw new MethodRefUnexpectedError(method.$ref, document);
});

(document.methods as MethodObject[]).forEach((method: MethodObject) => {
const params = method.params as ContentDescriptorObject[];
if (method.params === undefined) { return; }

params.forEach((param: ContentDescriptorObject) => {
if (param.schema === undefined) { return; }
Expand Down Expand Up @@ -70,37 +75,33 @@ export default class MethodCallValidator {
return new MethodNotFoundError(methodName, this.document, params);
}

if (method.params) {
const paramMap = (method.params as ContentDescriptorObject[]);
return compact(paramMap.map((param: ContentDescriptorObject, index: number): ParameterValidationError | undefined => {
let id: string | number;
if (method.paramStructure === "by-position") {
const paramMap = (method.params as ContentDescriptorObject[]);
return compact(paramMap.map((param: ContentDescriptorObject, index: number): ParameterValidationError | undefined => {
let id: string | number;
if (method.paramStructure === "by-position") {
id = index;
} else if (method.paramStructure === "by-name") {
id = param.name;
} else {
if (params[index] !== undefined) {
id = index;
} else if (method.paramStructure === "by-name") {
id = param.name;
} else {
if (params[index] !== undefined) {
id = index;
} else {
id = param.name;
}
id = param.name;
}
const input = params[id];
}
const input = params[id];

if (input === undefined && !param.required) { return; }
if (input === undefined && !param.required) { return; }

if (param.schema !== undefined) {
const idForMethod = generateMethodParamId(method, param);
const isValid = this.ajvValidator.validate(idForMethod, input);
const errors = this.ajvValidator.errors as ErrorObject[];
if (param.schema !== undefined) {
const idForMethod = generateMethodParamId(method, param);
const isValid = this.ajvValidator.validate(idForMethod, input);
const errors = this.ajvValidator.errors as ErrorObject[];

if (!isValid) {
return new ParameterValidationError(id, param.schema, input, errors);
}
if (!isValid) {
return new ParameterValidationError(id, param.schema, input, errors);
}
})) as ParameterValidationError[];
} else {
return [] as ParameterValidationError[];
}
}
})) as ParameterValidationError[];
}
}
Loading

0 comments on commit e659f42

Please sign in to comment.