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
299 changes: 299 additions & 0 deletions src/parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ReturnValue,
Service,
StringLiteral,
Type,
validate,
Violation,
} from 'basketry';
Expand Down Expand Up @@ -6255,6 +6256,304 @@ describe('parser', () => {
});
});

describe('intersections', () => {
it('creates types from an allOf with $refs', async () => {
// ARRANGE
const oas = {
openapi: '3.0.1',
info: { title: 'Test', version: '1.0.0', description: 'test' },
components: {
schemas: {
intersection: {
allOf: [
{ $ref: '#/components/schemas/typeA' },
{ $ref: '#/components/schemas/typeB' },
],
},
typeA: {
type: 'object',
properties: {
propFromA: { type: 'string', example: 'exampleValueA' },
},
},
typeB: {
type: 'object',
properties: {
propFromB: { type: 'number', example: 42 },
},
},
},
},
};

// ACT
const { service } = await parser(JSON.stringify(oas), absoluteSourcePath);

// ASSERT
expectService(service).toEqual(
partial<Service>({
types: exact([
partial<Type>({
kind: 'Type',
name: { value: 'intersection' },
properties: exact([
partial<Property>({
kind: 'Property',
name: { value: 'propFromA' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
partial<Property>({
kind: 'Property',
name: { value: 'propFromB' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'number' },
},
}),
]),
}),
partial<Type>({
kind: 'Type',
name: { value: 'typeA' },
properties: exact([
partial<Property>({
kind: 'Property',
name: { value: 'propFromA' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
]),
}),
partial<Type>({
kind: 'Type',
name: { value: 'typeB' },
properties: exact([
partial<Property>({
kind: 'Property',
name: { value: 'propFromB' },
}),
]),
}),
]),
}),
);
});

it('creates type from an allOf without $refs', async () => {
// ARRANGE
const oas = {
openapi: '3.0.1',
info: { title: 'Test', version: '1.0.0', description: 'test' },
components: {
schemas: {
intersection: {
allOf: [
{
type: 'object',
properties: {
propFromA: { type: 'string', example: 'exampleValueA' },
},
},
{
type: 'object',
properties: {
propFromB: { type: 'number', example: 42 },
},
},
],
},
},
},
};

// ACT
const { service } = await parser(JSON.stringify(oas), absoluteSourcePath);

// ASSERT
expectService(service).toEqual(
partial<Service>({
types: exact([
partial<Type>({
kind: 'Type',
name: { value: 'intersection' },
properties: exact([
partial<Property>({
kind: 'Property',
name: { value: 'propFromA' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
partial<Property>({
kind: 'Property',
name: { value: 'propFromB' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'number' },
},
}),
]),
}),
]),
}),
);
});

it('creates type from an allOf with duplicate properties', async () => {
// ARRANGE
const oas = {
openapi: '3.0.1',
info: { title: 'Test', version: '1.0.0', description: 'test' },
components: {
schemas: {
intersection: {
allOf: [
{
type: 'object',
properties: {
zprop1: { type: 'string' },
},
},
{
type: 'object',
properties: {
zprop1: { type: 'string' },
aprop2: { type: 'string' },
},
},
],
},
},
},
};

// ACT
const { service } = await parser(JSON.stringify(oas), absoluteSourcePath);

// ASSERT
expectService(service).toEqual(
partial<Service>({
types: exact([
partial<Type>({
kind: 'Type',
name: { value: 'intersection' },
properties: exact([
partial<Property>({
kind: 'Property',
name: { value: 'zprop1' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
partial<Property>({
kind: 'Property',
name: { value: 'aprop2' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
]),
}),
]),
}),
);
});

it('creates a type from an allOf keeping the last property by name', async () => {
// ARRANGE
const oas = {
openapi: '3.0.1',
info: { title: 'Test', version: '1.0.0', description: 'test' },
components: {
schemas: {
intersection: {
allOf: [
{
type: 'object',
properties: {
zprop1: { type: 'string' },
aprop2: { type: 'string' },
bprop3: { type: 'string' },
},
},
{
type: 'object',
properties: {
cprop4: { type: 'string' },
},
},
{
type: 'object',
properties: {
zprop1: { type: 'string' },
aprop2: { type: 'number' },
},
},
],
},
},
},
};

// ACT
const { service } = await parser(JSON.stringify(oas), absoluteSourcePath);

// ASSERT
expectService(service).toEqual(
partial<Service>({
types: exact([
partial<Type>({
kind: 'Type',
name: { value: 'intersection' },
properties: exact([
partial<Property>({
kind: 'Property',
name: { value: 'bprop3' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
partial<Property>({
kind: 'Property',
name: { value: 'cprop4' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
partial<Property>({
kind: 'Property',
name: { value: 'zprop1' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'string' },
},
}),
partial<Property>({
kind: 'Property',
name: { value: 'aprop2' },
value: {
kind: 'PrimitiveValue',
typeName: { value: 'number' },
},
}),
]),
}),
]),
}),
);
});
});

describe('unions', () => {
it('correctly parses a oneOf without $refs in a body parameter', async () => {
const oas = {
Expand Down
29 changes: 22 additions & 7 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1793,14 +1793,29 @@ export class OAS3Parser {
parentName?: string,
): Property[] {
if (allOf) {
return allOf.flatMap((subDef) => {
const resolved = this.resolve(subDef, OAS3.ObjectSchemaNode);
if (!resolved) return [];
const intersectedProperties = allOf
.flatMap((subDef) => {
const resolved = this.resolve(subDef, OAS3.ObjectSchemaNode);
if (!resolved) return [];

const p = resolved.properties;
const r = safeConcat(resolved.required, required);
return this.parseProperties(p, r, resolved.allOf, parentName);
})
.reverse();

const seenNames: Set<string> = new Set();
const uniqueProperties: Property[] = [];
for (const property of intersectedProperties) {
if (!seenNames.has(property.name.value)) {
seenNames.add(property.name.value);
uniqueProperties.push(property);
}
}

const p = resolved.properties;
const r = safeConcat(resolved.required, required);
return this.parseProperties(p, r, resolved.allOf, parentName);
});
const result = uniqueProperties.reverse();

return result;
} else {
const requiredSet = new Set<string>(required?.map((r) => r.value) || []);
const props: Property[] = [];
Expand Down