Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7,592 changes: 7,587 additions & 5 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-graph-core",
"version": "0.0.1-alpha.2.1.0",
"version": "0.0.1-alpha.3.0",
"description": "A TS library to manage large API projects defined by OpenAPIv3 specification.",
"main": "./lib/index.js",
"keywords": [
Expand Down Expand Up @@ -39,6 +39,6 @@
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.0.2",
"openapi-graph-types": "0.0.1-alpha.2.1.0"
"openapi-graph-types": "0.0.1-alpha.3.1"
}
}
36 changes: 36 additions & 0 deletions src/analyzer/Analyzer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
AnalyzerConstructor,
OpenAPIGraphInterface,
OpenAPIGraphsInterface,
SchemaNodeInterface,
} from 'openapi-graph-types';

export const Analyzer: AnalyzerConstructor = class AnalyzerImpl {
graphs!: { [key: string]: OpenAPIGraphInterface };

constructor(graphs: OpenAPIGraphsInterface) {
this.graphs = graphs.builder.graphs;
}

getUnusedSchemas(): { [path: string]: SchemaNodeInterface[] } {
const unusedSchemas: { [path: string]: SchemaNodeInterface[] } = {};
Object.keys(this.graphs).forEach((path) => {
const schemas = this.graphs[path].nodes.schemas;
unusedSchemas[path] = Object.values(schemas).filter(
(schema) => !schema.isInline && Object.values(schema.referencedBy).length === 0,
);
});
return unusedSchemas;
}

getDeprecatedSchemasBeingUsed(): { [path: string]: SchemaNodeInterface[] } {
const deprecatedSchemasBeingUsed: { [path: string]: SchemaNodeInterface[] } = {};
Object.keys(this.graphs).forEach((path) => {
const schemas = this.graphs[path].nodes.schemas;
deprecatedSchemasBeingUsed[path] = Object.values(schemas).filter(
(schema) => schema.content.type !== 'array' && schema.content.deprecated,
);
});
return deprecatedSchemasBeingUsed;
}
};
1 change: 1 addition & 0 deletions src/analyzer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Analyzer } from './Analyzer';
2 changes: 1 addition & 1 deletion src/graph/OpenAPIGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ export const OpenAPIGraph: OpenAPIGraphConstructor = class OpenAPIGraphImpl impl
getSchemaRefEdges(): EdgesRefDict['schemaRef'] {
return this.edges.ref.schemaRef;
}
}
};
4 changes: 2 additions & 2 deletions src/graph/OpenAPIGraphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const OpenAPIGraphs: OpenAPIGraphsConstructor = class OpenAPIGraphsImpl i
}

async build() {
const apis = await fetcher(this.rootPath)
const apis = await fetcher(this.rootPath);
this.builder = new OpenAPIGraphsBuilder(apis);
}
}
};
28 changes: 18 additions & 10 deletions src/graph/builder/OpenAPIGraphsBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,31 @@
import { getRefEdges, getSchemaNodes, resolveReference } from '.';
import { EdgesRefDict, Nodes, OpenAPIContent, OpenAPIGraphInterface, OpenAPIGraphsBuilderConstructor, OpenAPIGraphsBuilderInterface } from 'openapi-graph-types';
import {
EdgesRefDict,
Nodes,
OpenAPIContent,
OpenAPIGraphInterface,
OpenAPIGraphsBuilderConstructor,
OpenAPIGraphsBuilderInterface,
} from 'openapi-graph-types';
import { OpenAPIGraph } from '../OpenAPIGraph';

export const OpenAPIGraphsBuilder: OpenAPIGraphsBuilderConstructor = class OpenAPIGraphsBuilderImpl implements OpenAPIGraphsBuilderInterface {
graphs: OpenAPIGraphInterface[];
export const OpenAPIGraphsBuilder: OpenAPIGraphsBuilderConstructor = class OpenAPIGraphsBuilderImpl
implements OpenAPIGraphsBuilderInterface {
graphs: OpenAPIGraphsBuilderInterface['graphs'];

constructor(apis: OpenAPIContent[]) {
this.graphs = this.initializeGraph(apis);
}

private initializeGraph(apis: OpenAPIContent[]): OpenAPIGraphInterface[] {
const graphs: OpenAPIGraphInterface[] = [];
private initializeGraph(apis: OpenAPIContent[]): OpenAPIGraphsBuilderInterface['graphs'] {
const graphs: OpenAPIGraphsBuilderInterface['graphs'] = {};
apis.forEach((api) => {
const graph: OpenAPIGraphInterface = new OpenAPIGraph(api.path);
graph.setSchemaNodes(this.getSchemaNodes(api));
graphs.push(graph);
graphs[api.path] = graph;
});
graphs.map((graph, i) => {
graph.setRefEdges(this.getRefEdges(graphs, apis[i]));
Object.keys(graphs).map((graphKey, i) => {
graphs[graphKey].setRefEdges(this.getRefEdges(graphs, apis[i]));
});
return graphs;
}
Expand All @@ -26,8 +34,8 @@ export const OpenAPIGraphsBuilder: OpenAPIGraphsBuilderConstructor = class OpenA
return getSchemaNodes(api.content);
}

private getRefEdges(graphs: OpenAPIGraphInterface[], api: OpenAPIContent): EdgesRefDict {
private getRefEdges(graphs: OpenAPIGraphsBuilderInterface['graphs'], api: OpenAPIContent): EdgesRefDict {
const edges: EdgesRefDict = getRefEdges(api.content, api.path);
return resolveReference(graphs, edges);
}
}
};
95 changes: 76 additions & 19 deletions src/graph/builder/finder.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,84 @@
import { EdgesRefDict, Nodes, OpenAPIGraphsBuilderInterface, SchemaNodeInterface } from 'openapi-graph-types';
import { OpenAPIV3 } from 'openapi-types';
import { EdgesRefDict, Nodes, OpenAPIGraphInterface } from 'openapi-graph-types';
import { SchemaNode } from '../../graph/nodes/SchemaNode';
import { RefEdge } from '../edges';

/**
* Creates a new Schema depends on its type. Swagger schemas can be Array or NonArray
*
* @param schema source
* @param name The given name for the schema
* @returns the new schema instance
*/
function createSchema(
schemaNodes: { [key: string]: SchemaNodeInterface },
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
schemaName: string,
isInline: boolean,
) {
if (schema && !('$ref' in schema)) {
if ('type' in schema && schema?.type === 'array') {
schemaNodes[schemaName] = new SchemaNode(schemaName, schema, isInline);
} else {
schemaNodes[schemaName] = new SchemaNode(schemaName, schema, isInline);
}
}
}

export function getDefinedSchemasNodes(api: OpenAPIV3.Document): { [key: string]: SchemaNodeInterface } {
const nodes: { [key: string]: SchemaNodeInterface } = {};
const schemas: { [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject } | undefined =
api.components?.schemas;
if (schemas) {
Object.keys(schemas).forEach((schemaName) => createSchema(nodes, schemas[schemaName], schemaName, false));
}
return nodes;
}

/**
* It will find all the schemas defined in the specification
*
* @param api source
* @param fn callback which will be executed for every node
*/
export function getSchemaNodes(apiContent: OpenAPIV3.Document): Nodes['schemas'] {
const nodes: Nodes['schemas'] = {};
const schemas = apiContent?.components?.schemas;
if (schemas) {
Object.keys(schemas).forEach((schema) => {
if ('$ref' !== schema) {
nodes[schema] = new SchemaNode(schema, schemas[schema] as OpenAPIV3.SchemaObject);
}
});
export function getInlineSchemasNodes(
json: any,
currentIndex = 1,
nodes: { [key: string]: SchemaNodeInterface } = {},
): { [key: string]: SchemaNodeInterface } {
const schema: OpenAPIV3.SchemaObject | undefined = json?.schema;
if (schema) {
createSchema(nodes, schema, `inline-schema-${currentIndex++}`, true);
if (schema?.type === 'array') {
getInlineSchemasNodes(json.items, currentIndex, nodes);
}
} else if (json) {
function handleJson() {
Object.keys(json).forEach((key) => {
nodes = getInlineSchemasNodes(json[key], currentIndex, nodes);
});
}
if ({}.constructor === json.constructor) {
handleJson();
} else if ([].constructor === json.constructor) {
json.forEach(handleJson);
}
}
return nodes;
}

export function getSchemaNodes(api: OpenAPIV3.Document): Nodes['schemas'] {
return { ...getDefinedSchemasNodes(api), ...getInlineSchemasNodes(api) };
}

/**
* It will find all the references defined in the specification
*
* @param api source
* @param fn callback which will be executed for every node
*/
export function getRefEdges(json: any, absolutePath: string, edges: EdgesRefDict = { schemaRef: {} }): EdgesRefDict {
/* tslint:disable:no-string-literal */
const ref: string = json['$ref'];
const ref: string | undefined = json?.$ref;
if (ref) {
// TODO Should test any type of component
if (/components\/schemas/.test(ref)) {
Expand All @@ -39,7 +87,7 @@ export function getRefEdges(json: any, absolutePath: string, edges: EdgesRefDict
} else {
function handleJson() {
Object.keys(json).forEach((key) => {
edges = getRefEdges(json[key], absolutePath, edges);
getRefEdges(json[key], absolutePath, edges);
});
}
if ({}.constructor === json.constructor) {
Expand All @@ -51,17 +99,26 @@ export function getRefEdges(json: any, absolutePath: string, edges: EdgesRefDict
return edges;
}

export function resolveReference(graphs: OpenAPIGraphInterface[], refs: EdgesRefDict): EdgesRefDict {
/**
* Sets the edges' child value
* @param graphs
* @param refs
* @returns
*/
export function resolveReference(graphs: OpenAPIGraphsBuilderInterface['graphs'], refs: EdgesRefDict): EdgesRefDict {
const filteredRefs: EdgesRefDict = {
schemaRef: {},
};

Object.values(refs.schemaRef)
.map((r) => ({ r, g: graphs.find((g) => g.path === r.absolutePath) }))
.filter((o) => o.g?.nodes.schemas[o.r.tokenName])
.forEach((o) => {
o.r.child = o.g?.nodes.schemas[o.r.tokenName];
filteredRefs.schemaRef[o.r.getFullPath()] = o.r;
.filter((r) => graphs?.[r.refToFilePath].nodes.schemas[r.tokenName])
.forEach((r) => {
const schema = graphs?.[r.refToFilePath].nodes.schemas[r.tokenName];
if (schema) {
schema.referencedBy[r.path] = r;
r.child = schema;
filteredRefs.schemaRef[r.path] = r;
}
});

return filteredRefs;
Expand Down
53 changes: 51 additions & 2 deletions src/graph/edges/Edge.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,55 @@
import { EdgeConstructor, EdgeInterface, NodeInterface } from 'openapi-graph-types';
import { EdgeConstructor, EdgeInterface, NodeInterface, RefType } from 'openapi-graph-types';
import { resolve } from 'path';

export const Edge: EdgeConstructor = class EdgeImpl implements EdgeInterface {
parent: NodeInterface | undefined;
child: NodeInterface | undefined;
}

rawPath!: string;
tokenName!: string;
specificationPath!: string;
filePath!: string;
refToFilePath!: string;

constructor(filePath: string, specificationPath: string) {
// TODO: Check that inputs are valid
this.filePath = filePath;
this.rawPath = specificationPath;

const cleanSpecificationPathParts = specificationPath?.split('#');
if (cleanSpecificationPathParts.length < 2) {
return;
}
this.specificationPath = cleanSpecificationPathParts[1];
switch (this.type) {
case RefType.Local:
case RefType.URL:
this.refToFilePath = resolve(`${filePath}/${cleanSpecificationPathParts[0]}`);
break;
case RefType.Remote:
this.refToFilePath = resolve(`${filePath}/../${cleanSpecificationPathParts[0]}`);
break;
}
const specificationPathParts = specificationPath.split('/');
this.tokenName = specificationPathParts[specificationPathParts.length - 1];
}

get path(): string {
return `${this.filePath}#${this.specificationPath}`;
}

// TODO Tests
get type(): RefType | undefined {
// Test if extension file is in the string
if (/\.(yml|yaml|json)\/?#\//.test(this.rawPath)) {
if (/^https?:\/\//.test(this.rawPath)) {
return RefType.URL;
} else {
return RefType.Remote;
}
} else if (!/\.(yml|yaml|json)\/?#\//.test(this.rawPath)) {
return RefType.Local;
}
return undefined;
}
};
8 changes: 8 additions & 0 deletions src/graph/edges/InlineRefEdge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { RefEdgeConstructor, RefEdgeInterface } from 'openapi-graph-types';
import { Edge } from '.';

export const RefEdge: RefEdgeConstructor = class RefEdgeImpl extends Edge implements RefEdgeInterface {
constructor(absolutePath: string, ref: string) {
super(absolutePath, ref);
}
};
54 changes: 3 additions & 51 deletions src/graph/edges/RefEdge.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,8 @@
import { resolve } from 'path';
import { RefEdgeConstructor, RefEdgeInterface, RefType } from 'openapi-graph-types';
import { Edge } from '.';

export const RefEdge: RefEdgeConstructor = class RefEdgeImpl extends Edge implements RefEdgeInterface {
ref!: string;
absolutePath!: string;
tokenName!: string;
type!: RefType;

constructor(absolutePath: string, ref: string) {
super();

// Check that is valid ref and get its type
const type = this.getType(ref);
if (type === undefined) {
return;
}
this.type = type;
this.ref = ref;
this.tokenName = this.getTokenName(ref);
this.absolutePath = this.resolveAbsolutePath(absolutePath, ref);
}

// TODO Tests
getType(ref: string): RefType | undefined {
// Test if extension file is in the string
if (/\.(yml|yaml|json)\/?#\//.test(ref)) {
if (/^https?:\/\//.test(ref)) {
return RefType.URL;
} else {
return RefType.Remote;
}
} else if (!/\.(yml|yaml|json)\/?#\//.test(ref)) {
return RefType.Local;
}
return undefined;
}

// TODO Tests
getTokenName(ref: string): string {
const parts = ref.split('/');
return parts[parts.length - 1];
}

// TODO Tests
resolveAbsolutePath(absolutePath: string, ref: string): string {
const filePath = ref.split('#')[0];
return resolve(this.type === RefType.Remote ? `${absolutePath}/../${filePath}` : `${absolutePath}/${filePath}`);
}

// TODO Tests
getFullPath(): string {
return `${this.absolutePath}#${this.ref.split('#')[1]}`;
constructor(filePath: string, ref: string) {
super(filePath, ref);
}
}
};
4 changes: 2 additions & 2 deletions src/graph/nodes/Node.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { NodeConstructor, NodeInterface } from "openapi-graph-types";
import { NodeConstructor, NodeInterface } from 'openapi-graph-types';

export const Node: NodeConstructor = class NodeImpl implements NodeInterface {
name: string;

constructor(name: string) {
this.name = name;
}
}
};
Loading