feat: add bitbucket-cloud-common with new client
Add `bitbucket-cloud-common` (`@backstage/plugin-bitbucket-cloud-common`) with a new client for Bitbucket Cloud. The lib contains auto-generated models which got generated using `@openapitools/openapi-generator-cli`. Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const tsMorph = require('ts-morph');
|
||||
|
||||
function cleanupWrongAllOfModels(modelsModule) {
|
||||
const allOfInterfaces = modelsModule
|
||||
.getInterfaces()
|
||||
.filter(i => i.getName().includes('AllOf'));
|
||||
allOfInterfaces.forEach(i => {
|
||||
const name = i.getName();
|
||||
const realName = name.replace('AllOf', '');
|
||||
|
||||
const realInterface = modelsModule.getInterface(realName);
|
||||
if (realInterface) {
|
||||
i.remove();
|
||||
} else {
|
||||
i.rename(realName);
|
||||
}
|
||||
});
|
||||
|
||||
const allOfTypes = modelsModule
|
||||
.getTypeAliases()
|
||||
.filter(t => t.getName().includes('AllOf'));
|
||||
allOfTypes.forEach(t => {
|
||||
const name = t.getName();
|
||||
const realName = name.replace('AllOf', '');
|
||||
|
||||
const varStmt = modelsModule.getVariableStatementOrThrow(t.getName());
|
||||
|
||||
const realType = modelsModule.getTypeAlias(realName);
|
||||
if (realType) {
|
||||
t.remove();
|
||||
varStmt.remove();
|
||||
} else {
|
||||
t.rename(realName);
|
||||
varStmt.getDeclarationList().getDeclarations()[0].rename(realName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function makePaginatedGeneric(modelsModule) {
|
||||
const paginated = modelsModule.getInterface('Paginated');
|
||||
|
||||
if (paginated.getTypeParameters().length === 0) {
|
||||
paginated.addTypeParameter('TResultItem');
|
||||
}
|
||||
|
||||
const valuesProperty = paginated.getPropertyOrThrow('values');
|
||||
let valuesType = valuesProperty
|
||||
.getType()
|
||||
.getText()
|
||||
.replace('| null ', '')
|
||||
.replace('any[]', 'Array<TResultItem>')
|
||||
.replaceAll('any', 'TResultItem');
|
||||
if (valuesProperty.hasQuestionToken()) {
|
||||
valuesType = valuesType.replace(' | undefined', '');
|
||||
}
|
||||
valuesProperty.setType(valuesType);
|
||||
}
|
||||
|
||||
function setPaginatedResultItemType(modelsModule) {
|
||||
modelsModule
|
||||
.getInterfaces()
|
||||
.filter(i =>
|
||||
i.getExtends().find(it => it.getExpression().getText() === 'Paginated'),
|
||||
)
|
||||
.forEach(i => {
|
||||
const paginatedExtends = i
|
||||
.getExtends()
|
||||
.find(it => it.getExpression().getText() === 'Paginated');
|
||||
|
||||
const valuesProperty = i.getPropertyOrThrow('values');
|
||||
const resultItemType = valuesProperty
|
||||
.getType()
|
||||
.getUnionTypes()
|
||||
.map(it => it.getText())
|
||||
.find(it => it !== 'undefined')
|
||||
.replaceAll(/import[^ ]+.Models./g, '')
|
||||
.replace('[]', '')
|
||||
.replace(/(?:Array|Set)<(.*)>/, '$1');
|
||||
|
||||
paginatedExtends.setExpression(`Paginated<${resultItemType}>`);
|
||||
});
|
||||
}
|
||||
|
||||
const project = new tsMorph.Project({
|
||||
tsConfigFilePath: '../../tsconfig.json',
|
||||
skipAddingFilesFromTsConfig: true,
|
||||
});
|
||||
project.addSourceFilesAtPaths('src/**');
|
||||
|
||||
const modelsFile = project.getSourceFile('src/models/index.ts');
|
||||
const modelsModule = modelsFile.getModuleOrThrow('Models');
|
||||
|
||||
cleanupWrongAllOfModels(modelsModule);
|
||||
makePaginatedGeneric(modelsModule);
|
||||
setPaginatedResultItemType(modelsModule);
|
||||
|
||||
project.saveSync();
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2022 The Backstage Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR=$(dirname $0)
|
||||
PLUGIN_DIR="${SCRIPT_DIR}/.."
|
||||
|
||||
yarn --cwd "${PLUGIN_DIR}" openapi-generator-cli generate --generator-key backstage
|
||||
rm -d "${PLUGIN_DIR}/src/apis" # empty dir or fails
|
||||
"${SCRIPT_DIR}"/adjust-models.js
|
||||
yarn --cwd "${PLUGIN_DIR}" prettier --check . -w
|
||||
+279
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const BASE_DOMAIN = 'https://developer.atlassian.com';
|
||||
const SCHEMA_SOURCE = `${BASE_DOMAIN}/cloud/bitbucket/swagger.v3.json`;
|
||||
|
||||
const fetch = require('cross-fetch');
|
||||
const fs = require('fs');
|
||||
|
||||
const destFile = `${__dirname}/../bitbucket-cloud.oas.json`;
|
||||
|
||||
const sortSelectedProperties = (key, value) => {
|
||||
if (key !== 'schemas' && key !== 'properties') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.keys(value)
|
||||
.sort()
|
||||
.reduce((o, k) => {
|
||||
o[k] = value[k];
|
||||
return o;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
// similar to definition of "slug" at ""#/components/schemas/group"
|
||||
const repoSlugDefinition = {
|
||||
type: 'string',
|
||||
description:
|
||||
'The "sluggified" version of the repository\'s name. This contains only ASCII characters and can therefore be slightly different than the name',
|
||||
};
|
||||
|
||||
const paginatedDefinition = {
|
||||
type: 'object',
|
||||
title: 'Paginated',
|
||||
description: 'A generic paginated list.',
|
||||
discriminator: {
|
||||
propertyName: 'type',
|
||||
},
|
||||
properties: {
|
||||
next: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
description:
|
||||
'Link to the next page if it exists. The last page of a collection does not have this value. Use this link to navigate the result set and refrain from constructing your own URLs.',
|
||||
},
|
||||
page: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Page number of the current results. This is an optional element that is not provided in all responses.',
|
||||
},
|
||||
pagelen: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Current number of objects on the existing page. The default value is 10 with 100 being the maximum allowed value. Individual APIs may enforce different values.',
|
||||
},
|
||||
previous: {
|
||||
type: 'string',
|
||||
format: 'uri',
|
||||
description:
|
||||
'Link to previous page if it exists. A collections first page does not have this value. This is an optional element that is not provided in all responses. Some result sets strictly support forward navigation and never provide previous links. Clients must anticipate that backwards navigation is not always available. Use this link to navigate the result set and refrain from constructing your own URLs.',
|
||||
},
|
||||
size: {
|
||||
type: 'integer',
|
||||
description:
|
||||
'Total number of objects in the response. This is an optional element that is not provided in all responses, as it can be expensive to compute.',
|
||||
},
|
||||
values: {
|
||||
description: 'The values of the current page.',
|
||||
oneOf: [
|
||||
{
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
items: {},
|
||||
uniqueItems: false,
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
minItems: 0,
|
||||
items: {},
|
||||
uniqueItems: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const addMissingRepoSlug = json => {
|
||||
const repoProperties = json.components.schemas.repository.allOf.find(
|
||||
item => item.properties,
|
||||
).properties;
|
||||
if (repoProperties.slug) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'[WARN] repository schema already contains slug property. Patch got obsolete.',
|
||||
);
|
||||
} else {
|
||||
repoProperties.slug = repoSlugDefinition;
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
const removePageDefinition = json => {
|
||||
delete json.components.schemas.page;
|
||||
return json;
|
||||
};
|
||||
|
||||
const renamePaginatedSnippetCommitToPlural = json => {
|
||||
if (!json.components.schemas.paginated_snippet_commit) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
'[WARN] $.components.schemas.paginated_snippet_commit does not exist anymore. Patch got obsolete.',
|
||||
);
|
||||
return json;
|
||||
}
|
||||
|
||||
json.components.schemas.paginated_snippet_commits =
|
||||
json.components.schemas.paginated_snippet_commit;
|
||||
delete json.components.schemas.paginated_snippet_commit;
|
||||
|
||||
return JSON.parse(
|
||||
JSON.stringify(json).replace(
|
||||
'"#/components/schemas/paginated_snippet_commit"',
|
||||
'"#/components/schemas/paginated_snippet_commits"',
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const addPaginatedDefinition = json => {
|
||||
json.components.schemas.paginated = paginatedDefinition;
|
||||
return json;
|
||||
};
|
||||
|
||||
// Changes "interface PaginatedXyz {" to "interface PaginatedXyz extends Paginated<Xyz> {"
|
||||
// (generic type gets extracted from "values" property)
|
||||
const paginatedDefinitionsExtendPaginated = json => {
|
||||
// exception to the standard naming pattern PaginatedXyz / paginated_xyz
|
||||
const exceptions = [
|
||||
'deployments_ddev_paginated_environments',
|
||||
'deployments_stg_west_paginated_environments',
|
||||
'search_result_page',
|
||||
];
|
||||
|
||||
Object.keys(json.components.schemas)
|
||||
.filter(name => name.startsWith('paginated_') || exceptions.includes(name))
|
||||
.forEach(name => {
|
||||
// modify other paginated_[...] schemas
|
||||
const old = json.components.schemas[name];
|
||||
const title = old.title;
|
||||
const description = old.description;
|
||||
delete old.title;
|
||||
delete old.description;
|
||||
delete old.properties.page;
|
||||
delete old.properties.pagelen;
|
||||
delete old.properties.size;
|
||||
delete old.properties.previous;
|
||||
delete old.properties.next;
|
||||
delete old.additionalProperties;
|
||||
old.properties.values.description =
|
||||
old.properties.values.description ??
|
||||
paginatedDefinition.properties.values.description;
|
||||
|
||||
json.components.schemas[name] = {
|
||||
title: title,
|
||||
description: description,
|
||||
allOf: [
|
||||
{
|
||||
$ref: '#/components/schemas/paginated',
|
||||
},
|
||||
old,
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
const preventHardToDetectDuplicateInterfacesDueToAllOf = json => {
|
||||
Object.keys(json.components.schemas).forEach(name => {
|
||||
const schema = json.components.schemas[name];
|
||||
if (!schema.allOf) {
|
||||
return;
|
||||
}
|
||||
|
||||
schema.allOf.forEach(allOfItem => {
|
||||
if (allOfItem.title) {
|
||||
schema.title = schema.title ?? allOfItem.title;
|
||||
schema.description = schema.description ?? allOfItem.description;
|
||||
delete allOfItem.title;
|
||||
delete allOfItem.description;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
const removeBuggyDescription = json => {
|
||||
const valueProp =
|
||||
json.components.schemas.branchrestriction.allOf[1].properties.value;
|
||||
if (!valueProp.description.startsWith('<staticmethod')) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('[WARN] "removeBuggyDescription" is not necessary anymore.');
|
||||
return json;
|
||||
}
|
||||
|
||||
delete valueProp.description;
|
||||
return json;
|
||||
};
|
||||
|
||||
const resolveConflictingInheritance = json => {
|
||||
const schema = json.components.schemas.pipeline_selector;
|
||||
const extension = schema.allOf[1];
|
||||
delete schema.allOf;
|
||||
|
||||
json.components.schemas.pipeline_selector = {
|
||||
...schema,
|
||||
...extension,
|
||||
};
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
const escapeTsdocInDescription = json => {
|
||||
const prop = json.components.schemas.commitstatus.allOf[1].properties.url;
|
||||
prop.description = prop.description.replaceAll(/(\S*[{]\S+[}]\S*)/g, '`$1`');
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
const relativeToAbsoluteUrls = json => {
|
||||
Object.keys(json.components.schemas).forEach(name => {
|
||||
const schema = json.components.schemas[name];
|
||||
if (schema.description) {
|
||||
schema.description = schema.description.replace(
|
||||
/]\(\/cloud\/bitbucket\//g,
|
||||
`](${BASE_DOMAIN}/cloud/bitbucket/`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
fetch(SCHEMA_SOURCE)
|
||||
.then(res => res.json())
|
||||
.then(addMissingRepoSlug)
|
||||
.then(removePageDefinition)
|
||||
.then(renamePaginatedSnippetCommitToPlural)
|
||||
.then(addPaginatedDefinition)
|
||||
.then(paginatedDefinitionsExtendPaginated)
|
||||
.then(preventHardToDetectDuplicateInterfacesDueToAllOf)
|
||||
.then(removeBuggyDescription)
|
||||
.then(resolveConflictingInheritance)
|
||||
.then(escapeTsdocInDescription)
|
||||
.then(relativeToAbsoluteUrls)
|
||||
.then(json => {
|
||||
fs.writeFileSync(destFile, JSON.stringify(json, sortSelectedProperties, 2));
|
||||
return json;
|
||||
});
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const tsMorph = require('ts-morph');
|
||||
|
||||
const project = new tsMorph.Project({
|
||||
tsConfigFilePath: '../../tsconfig.json',
|
||||
skipAddingFilesFromTsConfig: true,
|
||||
});
|
||||
project.addSourceFilesAtPaths('src/**');
|
||||
|
||||
const modelsFile = project.getSourceFile('src/models/index.ts');
|
||||
const modelsModule = modelsFile.getModuleOrThrow('Models');
|
||||
|
||||
const clientFile = project.getSourceFile('src/BitbucketCloudClient.ts');
|
||||
const clientClass = clientFile.getClassOrThrow('BitbucketCloudClient');
|
||||
|
||||
/**
|
||||
* Returns an array of the unique items of the provided array.
|
||||
*
|
||||
* @param {string[]} array array with potentially non-unique items.
|
||||
* @returns {string[]} array with unique items.
|
||||
*/
|
||||
function unique(array) {
|
||||
return [...new Set(array)];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {tsMorph.ClassDeclaration | tsMorph.InterfaceDeclaration | tsMorph.TypeAliasDeclaration} stmt Statement like interface or type alias.
|
||||
* @param {string[]=} processed Keeps track of which statement was already processed.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function referencedModelsIdentifiers(stmt, processed) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
processed = processed ?? [];
|
||||
const name = stmt.getName();
|
||||
|
||||
if (processed.includes(name)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const referenced = unique(
|
||||
stmt
|
||||
.getDescendantsOfKind(tsMorph.SyntaxKind.Identifier)
|
||||
.map(it => it.getSymbol())
|
||||
.filter(it => it)
|
||||
.map(it => it.getFullyQualifiedName())
|
||||
.filter(it => it.includes('Models.'))
|
||||
.map(it => it.substring(it.indexOf('Models.') + 7))
|
||||
.filter(it => !it.includes('.'))
|
||||
.filter(it => it !== name),
|
||||
);
|
||||
processed.push(name);
|
||||
|
||||
const transitivelyReferenced = referenced
|
||||
.map(
|
||||
it =>
|
||||
modelsModule.getInterface(it) ?? modelsModule.getTypeAliasOrThrow(it),
|
||||
)
|
||||
.flatMap(it => referencedModelsIdentifiers(it, processed));
|
||||
|
||||
return unique([...referenced, ...transitivelyReferenced]);
|
||||
}
|
||||
|
||||
// all directly or transitively referenced/used `Models.[...]` are allowed to stay
|
||||
const allowed = referencedModelsIdentifiers(clientClass);
|
||||
|
||||
// remove everything not part of the "allow list"
|
||||
modelsModule
|
||||
.getInterfaces()
|
||||
.filter(it => !allowed.includes(it.getName()))
|
||||
.forEach(it => it.remove());
|
||||
|
||||
modelsModule
|
||||
.getTypeAliases()
|
||||
.filter(it => !allowed.includes(it.getName()))
|
||||
.forEach(it => {
|
||||
const varStmt = modelsModule.getVariableStatementOrThrow(it.getName());
|
||||
|
||||
it.remove();
|
||||
varStmt.remove();
|
||||
});
|
||||
|
||||
project.saveSync();
|
||||
Reference in New Issue
Block a user