Skip to content

Commit

Permalink
Merge pull request #370 from AikidoSec/improve-body-schema
Browse files Browse the repository at this point in the history
API Discovery: Support merging of primitive data types
  • Loading branch information
willem-delbare authored Sep 18, 2024
2 parents 9a3f57e + 5990acb commit 006bad8
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 8 deletions.
10 changes: 10 additions & 0 deletions library/agent/api-discovery/isPrimitiveType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function onlyContainsPrimitiveTypes(types: string | string[]): boolean {
if (!Array.isArray(types)) {
return isPrimitiveType(types);
}
return types.every(isPrimitiveType);
}

function isPrimitiveType(type: string): boolean {
return !["object", "array"].includes(type);
}
151 changes: 151 additions & 0 deletions library/agent/api-discovery/mergeDataSchemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,154 @@ t.test("it prefers non-null type", async (t) => {
}
);
});

t.test("empty array", async (t) => {
t.same(mergeDataSchemas(getDataSchema([]), getDataSchema([])), {
type: "array",
items: undefined,
});
});

t.test("it merges types", async (t) => {
t.same(mergeDataSchemas(getDataSchema("str"), getDataSchema(15)), {
type: ["string", "number"],
});

// Can not merge object with primitive type
t.same(
mergeDataSchemas(
getDataSchema({
test: "abc",
}),
getDataSchema(15)
),
{
type: "object",
properties: {
test: {
type: "string",
},
},
}
);

t.same(
mergeDataSchemas(
getDataSchema({
test: "abc",
}),
getDataSchema({
test: true,
})
),
{
type: "object",
properties: {
test: {
type: ["string", "boolean"],
},
},
}
);

t.same(
mergeDataSchemas(
getDataSchema({
test: "abc",
}),
mergeDataSchemas(
getDataSchema({
test: "abc",
}),
getDataSchema({
test: true,
})
)
),
{
type: "object",
properties: {
test: {
type: ["string", "boolean"],
},
},
}
);

t.same(
mergeDataSchemas(
mergeDataSchemas(
getDataSchema({
test: true,
}),
getDataSchema({
test: "test",
})
),
getDataSchema({
test: "abc",
})
),
{
type: "object",
properties: {
test: {
type: ["boolean", "string"],
},
},
}
);

t.same(
mergeDataSchemas(
getDataSchema({
test: "abc",
}),
mergeDataSchemas(
getDataSchema({
test: 123,
}),
getDataSchema({
test: true,
})
)
),
{
type: "object",
properties: {
test: {
type: ["string", "number", "boolean"],
},
},
}
);

t.same(
mergeDataSchemas(
mergeDataSchemas(
getDataSchema({
test: "test",
}),
getDataSchema({
test: true,
})
),
mergeDataSchemas(
getDataSchema({
test: 123,
}),
getDataSchema({
test: true,
})
)
),
{
type: "object",
properties: {
test: {
type: ["string", "boolean", "number"],
},
},
}
);
});
79 changes: 71 additions & 8 deletions library/agent/api-discovery/mergeDataSchemas.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
import { DataSchema } from "./getDataSchema";
import { onlyContainsPrimitiveTypes } from "./isPrimitiveType";

/**
* Merge two data schemas into one, getting all properties from both schemas to capture optional properties.
* If the types are different, a merge is not possible and the first schema is returned. (Except one is null, then the other is returned)
* The first schema is preferred over the second schema because it can already contain multiple merged schemas.
* If the types are different, only primitive types are merged.
* Merging of arrays with objects or objects / arrays with primitive types is not supported.
* In this case the first schema is preferred over the second schema because it can already contain multiple merged schemas.
* If the types are the same, the properties of the second schema are merged into the first schema.
*/
export function mergeDataSchemas(first: DataSchema, second: DataSchema) {
const result: DataSchema = { ...first };

// Can not merge different types
if (first.type !== second.type) {
// Prefer non-null type
if (first.type === "null") {
return { ...second };
}
return result;
if (!isSameType(first.type, second.type)) {
return mergeTypes(first, second);
}

if (first.properties && second.properties) {
Expand Down Expand Up @@ -49,3 +47,68 @@ export function mergeDataSchemas(first: DataSchema, second: DataSchema) {

return result;
}

/**
* Check if both types are the same.
*/
function isSameType(
first: string | string[],
second: string | string[]
): boolean {
if (Array.isArray(first) && Array.isArray(second)) {
return doTypeArraysMatch(first, second);
}

if (Array.isArray(first) && !Array.isArray(second)) {
return doTypeArraysMatch(first, [second]);
}

if (!Array.isArray(first) && Array.isArray(second)) {
return doTypeArraysMatch([first], second);
}

return first === second;
}

/**
* Compare two arrays of types and ignore the order.
*/
function doTypeArraysMatch(first: string[], second: string[]): boolean {
if (first.length !== second.length) {
return false;
}

return first.every((type) => second.includes(type));
}

/**
* Merge types into one schema if they are different.
*/
function mergeTypes(first: DataSchema, second: DataSchema): DataSchema {
// Currently we do not support merging arrays and other objects and arrays / objects with primitive types
if (
!onlyContainsPrimitiveTypes(first.type) ||
!onlyContainsPrimitiveTypes(second.type)
) {
// Prefer non-null type
if (first.type === "null") {
return second;
}
return first;
}

first.type = mergeTypeArrays(first.type, second.type);
return first;
}

function mergeTypeArrays(first: string | string[], second: string | string[]) {
if (!Array.isArray(first)) {
first = [first];
}

if (!Array.isArray(second)) {
second = [second];
}

return Array.from(new Set([...first, ...second]));
}

0 comments on commit 006bad8

Please sign in to comment.