Skip to content

Commit

Permalink
Merge pull request #2 from sbcgua/apack-deep
Browse files Browse the repository at this point in the history
Apack deep, to #1 
parsing of apack dependencies
  • Loading branch information
sbcgua authored Mar 1, 2020
2 parents 20e8632 + 71b9849 commit 77bebd6
Show file tree
Hide file tree
Showing 17 changed files with 507 additions and 168 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
node_modules
jspm_packages

# artifacts
coverage

# Serverless directories
.serverless

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ For example: [`https://img.shields.io/endpoint?url=https://shield.abap.space/ver

- The version is supposed to be in semantic version format - `'X.Y.Z'` or `'vX.Y.Z'`.
- if `$PATH` = `.apack-manifest.xml` the version is read directly from that file.
- apack parsing also supports displaying dependency version (see [issue #1](https://github.com/sbcgua/abap-package-version-shield/issues/1)). `'...apack-manifest.xml/dependencies/<group_id>/<artifact_id>'`.

### Badge customizing

Expand Down
5 changes: 4 additions & 1 deletion handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const {
const {
APACK_FILENAME,
getVersionFromApack,
getDependencyVersionFromApack,
} = require('./lib/apack');

module.exports.getShieldJson = async (event, context) => {
Expand Down Expand Up @@ -45,7 +46,9 @@ async function handleEvent(event, context) {
const url = createUrlFromParams(validatedParams);
const srcData = await fetchResource(url);
let version = (validatedParams.file === APACK_FILENAME)
? getVersionFromApack(srcData)
? validatedParams.apackExtra === 'dependencies'
? getDependencyVersionFromApack(srcData, validatedParams.apackExtraParam)
: getVersionFromApack(srcData)
: parseSourceFile(srcData, validatedParams.attr);

validateVersion(version);
Expand Down
44 changes: 42 additions & 2 deletions handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ https.get.mockImplementation((url, handler) => {
resMock.write(' <asx:values>');
resMock.write(' <DATA>');
resMock.write(' <VERSION>0.2</VERSION>');
resMock.write(' <DEPENDENCIES>');
resMock.write(' <item>');
resMock.write(' <GROUP_ID>sap.com</GROUP_ID>');
resMock.write(' <ARTIFACT_ID>abap-platform-XX</ARTIFACT_ID>');
resMock.write(' <VERSION>1.2.0</VERSION>');
resMock.write(' <GIT_URL>https://github.com/SAP/abap-platform-xx.git</GIT_URL>');
resMock.write(' </item>');
resMock.write(' </DEPENDENCIES>');
resMock.write(' </DATA>');
resMock.write(' </asx:values>');
resMock.write('</asx:abap>');
Expand Down Expand Up @@ -69,8 +77,6 @@ describe('test with path params', () => {

test('should fail with wrong request', async () => {
const event = {
// resource: '/version-shield-json/{sourcePath}',
// path: '/version-shield-json/xxx',
pathParameters: {
sourcePath: 'xxx'
},
Expand Down Expand Up @@ -158,4 +164,38 @@ describe('test with apack', () => {
expect(console.log).toHaveBeenNthCalledWith(2, 'URL:', 'https://raw.githubusercontent.com/zzz/apack-test/master/.apack-manifest.xml');
expect(console.log).toHaveBeenNthCalledWith(3, 'fetch statusCode: 200');
});

test('should work with apack and dependencies', async () => {
const event = {
resource: '/version-shield-json/{sourcePath}',
path: '/version-shield-json/github/zzz/apack-test/.apack-manifest.xml',
pathParameters: {
sourcePath: 'github/zzz/apack-test/.apack-manifest.xml/dependencies/sap.com/abap-platform-xx'
},
};
const context = {};

global.console = {
log: jest.fn(),
error: jest.fn(),
};

await expect(handler.getShieldJson(event, context)).resolves.toEqual({
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: 'v1.2.0',
schemaVersion: 1,
label: 'abap package version',
color: 'orange',
}),
});

expect(console.log).toHaveBeenNthCalledWith(1, 'Requested path:', event.path);
expect(console.log).toHaveBeenNthCalledWith(2, 'URL:', 'https://raw.githubusercontent.com/zzz/apack-test/master/.apack-manifest.xml');
expect(console.log).toHaveBeenNthCalledWith(3, 'fetch statusCode: 200');
});
});
8 changes: 8 additions & 0 deletions jest.e2e.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testMatch: [
'**/?(*.)+(e2e).(test).[jt]s?(x)',
],
// "testPathIgnorePatterns": [
// "/node_modules/",
// ]
};
33 changes: 33 additions & 0 deletions lambda.e2e.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const fetch = require('node-fetch');
const pick = require('lodash.pick');

const PREFIX = (process.env.E2E_DEV === '1') ? 'dev.' : '';
const HOST = PREFIX + 'shield.abap.space';
const functionName = 'version-shield-json';
const versionRe = /^v\d{1,3}\.\d{1,3}(\.\d{1,3})?$/i;
console.log('Host:', HOST);

const getUrl = (params) => `https://${HOST}/${functionName}/${params}`;

async function validateExpectations(resp) {
expect(resp.ok).toBeTruthy();
const json = await resp.json();
expect(typeof json).toBe('object');
expect(pick(json, ['schemaVersion', 'label', 'color'])).toEqual({
schemaVersion: 1,
label: 'abap package version',
color: 'orange'
});
expect(typeof json.message).toBe('string');
expect(json.message).toMatch(versionRe);
}

test('should process abap constant', async () => {
const resp = await fetch(getUrl('github/sbcgua/mockup_loader/src/zif_mockup_loader_constants.intf.abap'));
await validateExpectations(resp);
});

test('should process apack', async () => {
const resp = await fetch(getUrl('github/SAP-samples/abap-platform-jak/.apack-manifest.xml'));
await validateExpectations(resp);
});
43 changes: 34 additions & 9 deletions lib/apack.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,48 @@
const xml = require('xml-parse');
var parser = require('fast-xml-parser');
const APACK_FILENAME = '.apack-manifest.xml';
const { xmlGetChildrenOf } = require('./utils');

function getVersionFromApack(str) {
let parsedXML;
function parseXml(xmlStr) {
try {
parsedXML = xml.parse(str);
return parser.parse(xmlStr, {
parseNodeValue : false,
arrayMode: false,
});
} catch (error) {
throw Error('apack xml parsing error');
}
}

function getVersionFromApack(xmlStr) {
const parsedXML = parseXml(xmlStr);
const versionNode = xmlGetChildrenOf(parsedXML, 'asx:abap/asx:values/DATA/VERSION');
if (!versionNode || typeof versionNode !== 'string') throw Error('wrong apack xml structure');
return versionNode;
}

function getDependencyVersionFromApack(xmlStr, depName) {
if (!depName || typeof depName !== 'string') throw Error('Incorrect dependency name');
depName = depName.toLowerCase();
const parsedXML = parseXml(xmlStr);

// Get deps node
const data = xmlGetChildrenOf(parsedXML, 'asx:abap/asx:values/DATA');
if (!data.DEPENDENCIES
|| !data.DEPENDENCIES.item
|| typeof data.DEPENDENCIES.item !== 'object' ) throw Error('dependency not found');
if (!Array.isArray(data.DEPENDENCIES.item)) data.DEPENDENCIES.item = [data.DEPENDENCIES.item]; // for a case of one item
const dependencies = data.DEPENDENCIES.item.filter(i => typeof i === 'object');

// Find target
const target = dependencies.find(i => [i.GROUP_ID, i.ARTIFACT_ID].join('/').toLowerCase() === depName);
if (!target) throw Error('dependency not found');
if (!target.VERSION) throw Error('dependency version not found');

const versionNodeChildren = xmlGetChildrenOf(parsedXML, 'DATA/VERSION');
if (!versionNodeChildren) throw Error('wrong apack xml structure');
const versionText = versionNodeChildren.find(node => node.type === 'text');
if (!versionText) throw Error('wrong apack xml structure');
return versionText.text;
return target.VERSION;
}

module.exports = {
APACK_FILENAME,
getVersionFromApack,
getDependencyVersionFromApack,
};
70 changes: 68 additions & 2 deletions lib/apack.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const { getVersionFromApack } = require('./apack');
const {
getVersionFromApack,
getDependencyVersionFromApack,
} = require('./apack');

const XML_SAMPLE = `
<?xml version="1.0" encoding="utf-8"?>
Expand All @@ -14,8 +17,15 @@ const XML_SAMPLE = `
<item>
<GROUP_ID>sap.com</GROUP_ID>
<ARTIFACT_ID>abap-platform-yy</ARTIFACT_ID>
<VERSION>1.1.0</VERSION>
<GIT_URL>https://github.com/SAP/abap-platform-yy.git</GIT_URL>
</item>
<item>
<GROUP_ID>sap.com</GROUP_ID>
<ARTIFACT_ID>abap-platform-XX</ARTIFACT_ID>
<VERSION>1.2.0</VERSION>
<GIT_URL>https://github.com/SAP/abap-platform-xx.git</GIT_URL>
</item>
</DEPENDENCIES>
</DATA>
</asx:values>
Expand All @@ -27,5 +37,61 @@ test('should getVersionFromApack', () => {
});

test('should fail on incorrect XML', () => {
expect(() => getVersionFromApack(XML_SAMPLE.replace('VERSION', 'NOTVERSION'))).toThrow('wrong apack xml structure');
const malformedXml = XML_SAMPLE.replace('VERSION', 'NOTVERSION');
expect(() => getVersionFromApack(malformedXml))
.toThrow('wrong apack xml structure');
});

test('should get version of a dependency', () => {
expect(getDependencyVersionFromApack(XML_SAMPLE, 'sap.com/abap-platform-xx')).toBe('1.2.0');
});

test('should get version of a dependency where there is one dependency', () => {
const XML_SAMPLE = `
<?xml version="1.0" encoding="utf-8"?>
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
<asx:values>
<DATA>
<DEPENDENCIES>
<item>
<GROUP_ID>sap.com</GROUP_ID>
<ARTIFACT_ID>abap-platform-XX</ARTIFACT_ID>
<VERSION>1.2.0</VERSION>
<GIT_URL>https://github.com/SAP/abap-platform-xx.git</GIT_URL>
</item>
</DEPENDENCIES>
</DATA>
</asx:values>
</asx:abap>
`;
expect(getDependencyVersionFromApack(XML_SAMPLE, 'sap.com/abap-platform-xx')).toBe('1.2.0');
});

test('should fail on dependency without version', () => {
const malformedXml = XML_SAMPLE.replace(/VERSION/g, 'NOTVERSION');
expect(() => getDependencyVersionFromApack(malformedXml, 'sap.com/abap-platform-xx'))
.toThrow('dependency version not found');
});

test('should fail on missing dependency', () => {
expect(() => getDependencyVersionFromApack(XML_SAMPLE, 'sap.com/abap-platform-zz'))
.toThrow('dependency not found');
});

test('should fail on missing dependencies node', () => {
expect(() => getDependencyVersionFromApack(`
<?xml version="1.0" encoding="utf-8"?>
<asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
<asx:values>
<DATA>
<GROUP_ID>sap.com</GROUP_ID>
<ARTIFACT_ID>abap-platform-jak</ARTIFACT_ID>
<VERSION>0.2</VERSION>
<REPOSITORY_TYPE>abapGit</REPOSITORY_TYPE>
<GIT_URL>https://github.com/SAP/abap-platform-jak.git</GIT_URL>
</DATA>
</asx:values>
</asx:abap>
`, 'sap.com/abap-platform-zz'))
.toThrow('dependency not found');
});
42 changes: 29 additions & 13 deletions lib/params.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
const {
enumify,
} = require('./utils');
const { APACK_FILENAME } = require('./apack');
const pick = require('lodash.pick');

function parsePathParams({pathParameters}) {
if (!pathParameters) throw Error('Unexpected path');
if (!pathParameters.sourcePath) throw Error('Unexpected source path');
const segments = pathParameters.sourcePath.split('/').filter(s => s !== '');
if (segments.length === 0) throw Error('Unexpected path');
if (segments.length > 20) throw Error('Too many path segments');

const STATES = enumify(['TYPE', 'OWNER', 'REPO', 'FILE', 'ATTR', 'END']);
let expected = STATES.TYPE;
let params = {};
for (const seg of segments) {
const appendSeg = (param) => param ? (param + '/' + seg) : seg;
switch (expected) {
case STATES.TYPE:
params.type = seg;
Expand All @@ -26,13 +30,17 @@ function parsePathParams({pathParameters}) {
expected++;
break;
case STATES.FILE:
if (/\.abap$/i.test(seg)) expected++;
params.file ? (params.file += '/') : (params.file = '');
params.file += seg;
params.file = appendSeg(params.file);
if (/\.abap$/i.test(seg) || seg === APACK_FILENAME) expected++;
break;
case STATES.ATTR:
params.attr = seg;
expected++;
if (params.file === APACK_FILENAME) {
if (!params.apackExtra) params.apackExtra = seg;
else params.apackExtraParam = appendSeg(params.apackExtraParam);
} else {
params.attr = seg;
expected++;
}
break;
case STATES.END:
throw Error('Unexpect path segment');
Expand All @@ -43,6 +51,12 @@ function parsePathParams({pathParameters}) {
return params;
}

function applyDefaults(params) {
const final = {...params};
if (!final.attr && final.file !== APACK_FILENAME) final.attr = 'version';
return final;
}

function validateQueryParams(params) {
if (!params.type) throw Error('Repository type not specified'); // 400 bad request
if (!params.owner) throw Error('Owner not specified');
Expand All @@ -52,19 +66,21 @@ function validateQueryParams(params) {
const supportedTypes = ['github'];
if (!supportedTypes.includes(params.type)) throw Error('Repository type not supported');

if ((params.apackExtra || params.apackExtraParam) && params.file !== APACK_FILENAME) throw Error('Apack params consistency failed');

const supportedApackExtraActions = ['dependencies'];
if (params.apackExtra && !supportedApackExtraActions.includes(params.apackExtra)) throw Error('Wrong apack extra action');

const allAttrs = ['type', 'owner', 'repo', 'file', 'attr', 'apackExtra', 'apackExtraParam'];
const allowedSymbols = /^[-_.,0-9a-zA-Z/]+$/;
for (const attr of ['owner', 'repo', 'file', 'attr']) {
for (const attr of allAttrs) {
if (!Object.prototype.hasOwnProperty.call(params, attr)) continue;
if (!allowedSymbols.test(params[attr])) throw Error(`[${attr}] has disallowed symbols`);
}

return {
type: params.type,
owner: params.owner,
repo: params.repo,
file: params.file,
attr: params.attr || 'version',
};
if (params.apackExtra === 'dependencies' && !params.apackExtraParam) throw Error('dependencies action expect params');

return applyDefaults(pick(params, allAttrs));
}

module.exports = {
Expand Down
Loading

0 comments on commit 77bebd6

Please sign in to comment.