Skip to content

Commit

Permalink
[FEATURE] Add Configuration (#575)
Browse files Browse the repository at this point in the history
Introduce a new Configuration module for persisting UI5 Project specific settings.

Complements: #570
JIRA: CPOUI5FOUNDATION-634
Co-authored-by: Yavor Ivanov <yavor.ivanov@sap.com>
  • Loading branch information
RandomByte and d3xter666 authored Apr 19, 2023
1 parent 6a09bc9 commit fd37cef
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 4 deletions.
93 changes: 93 additions & 0 deletions lib/config/Configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import path from "node:path";
import os from "node:os";

/**
* Provides basic configuration settings for @ui5/project/ui5Framework/* resolvers.
* Reads/writes configuration from/to ~/.ui5rc
*
* @public
* @class
* @alias @ui5/project/config/Configuration
*/
class Configuration {
#mavenSnapshotEndpointUrl;

/**
* @param {object} configuration
* @param {string} [configuration.mavenSnapshotEndpointUrl]
*/
constructor({mavenSnapshotEndpointUrl}) {
this.#mavenSnapshotEndpointUrl = mavenSnapshotEndpointUrl;
}

/**
* Maven Repository Snapshot URL.
* Used to download artifacts and packages from Maven's build-snapshots URL.
*
* @public
* @returns {string}
*/
getMavenSnapshotEndpointUrl() {
return this.#mavenSnapshotEndpointUrl;
}

/**
* @public
* @returns {object} The configuration in a JSON format
*/
toJSON() {
return {
mavenSnapshotEndpointUrl: this.#mavenSnapshotEndpointUrl,
};
}

/**
* Creates Configuration from a JSON file
*
* @public
* @static
* @param {string} [filePath="~/.ui5rc"] Path to configuration JSON file
* @returns {Promise<@ui5/project/config/Configuration>} Configuration instance
*/
static async fromFile(filePath) {
filePath = filePath || path.resolve(path.join(os.homedir(), ".ui5rc"));

const {default: fs} = await import("graceful-fs");
const {promisify} = await import("node:util");
const readFile = promisify(fs.readFile);
let config;
try {
const fileContent = await readFile(filePath);
config = JSON.parse(fileContent);
} catch (err) {
if (err.code === "ENOENT") {
// "File or directory does not exist"
config = {};
} else {
throw err;
}
}
return new Configuration(config);
}

/**
* Saves Configuration to a JSON file
*
* @public
* @static
* @param {@ui5/project/config/Configuration} config Configuration to save
* @param {string} [filePath="~/.ui5rc"] Path to configuration JSON file
* @returns {Promise<void>}
*/
static async toFile(config, filePath) {
filePath = filePath || path.resolve(path.join(os.homedir(), ".ui5rc"));

const {default: fs} = await import("graceful-fs");
const {promisify} = await import("node:util");
const writeFile = promisify(fs.writeFile);

return writeFile(filePath, JSON.stringify(config.toJSON()));
}
}

export default Configuration;
2 changes: 1 addition & 1 deletion lib/ui5Framework/Sapui5MavenSnapshotResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class Sapui5MavenSnapshotResolver extends AbstractResolver {
if (ok) {
log.info(`Using Maven snapshot endpoint URL resolved from Maven configuration file: ${url}`);
log.info(`Consider persisting this choice by executing the following command: ` +
`ui5 config set snapshotEndpointUrl ${url}`);
`ui5 config set mavenSnapshotEndpointUrl ${url}`);
} else {
log.verbose(`User rejected usage of the resolved URL`);
url = null;
Expand Down
2 changes: 1 addition & 1 deletion lib/ui5Framework/maven/Installer.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class Installer extends AbstractInstaller {
reject(new Error(
`Installer: Missing or empty Maven repository URL for snapshot consumption. ` +
`Please configure the correct URL using the following command: ` +
`ui5 config set snapshotEndpointUrl https://registry.corp/vendor/build-snapshots/`));
`ui5 config set mavenSnapshotEndpointUrl https://registry.corp/vendor/build-snapshots/`));
}

resolve(new Registry({endpointUrl: snapshotEndpointUrl}));
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
],
"type": "module",
"exports": {
"./config/Configuration": "./lib/config/Configuration.js",
"./specifications/Specification": "./lib/specifications/Specification.js",
"./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js",
"./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js",
Expand Down
122 changes: 122 additions & 0 deletions test/lib/config/Configuration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import test from "ava";
import sinonGlobal from "sinon";
import esmock from "esmock";

test.beforeEach(async (t) => {
const sinon = t.context.sinon = sinonGlobal.createSandbox();

t.context.homedirStub = sinon.stub().returns("~");
t.context.promisifyStub = sinon.stub();
t.context.resolveStub = sinon.stub().callsFake((path) => path);
t.context.joinStub = sinon.stub().callsFake((...args) => args.join("/"));
t.context.Configuration = await esmock.p("../../../lib/config/Configuration.js", {
"node:path": {
resolve: t.context.resolveStub,
join: t.context.joinStub
},
"node:util": {
"promisify": t.context.promisifyStub
},
"node:os": {
"homedir": t.context.homedirStub
}
});
});

test.afterEach.always((t) => {
t.context.sinon.restore();
esmock.purge(t.context.Configuration);
});

test.serial("Build configuration with defaults", (t) => {
const {Configuration} = t.context;

const config = new Configuration({});

t.deepEqual(config.toJSON(), {
mavenSnapshotEndpointUrl: undefined
});
});


test.serial("Overwrite defaults defaults", (t) => {
const {Configuration} = t.context;

const params = {
mavenSnapshotEndpointUrl: "https://snapshot.url"
};

const config = new Configuration(params);

t.deepEqual(config.toJSON(), params);
});

test.serial("Check getters", (t) => {
const {Configuration} = t.context;

const params = {
mavenSnapshotEndpointUrl: "https://snapshot.url"
};

const config = new Configuration(params);

t.is(config.getMavenSnapshotEndpointUrl(), params.mavenSnapshotEndpointUrl);
});


test.serial("fromFile", async (t) => {
const fromFile = t.context.Configuration.fromFile;
const {promisifyStub, sinon} = t.context;

const ui5rcContents = {
mavenSnapshotEndpointUrl: "https://snapshot.url"
};
const responseStub = sinon.stub().resolves(JSON.stringify(ui5rcContents));
promisifyStub.callsFake(() => responseStub);

const config = await fromFile("/custom/path/.ui5rc");

t.deepEqual(config.toJSON(), ui5rcContents);
});

test.serial("fromFile: configuration file not found- fallback to default config", async (t) => {
const {promisifyStub, sinon, Configuration} = t.context;
const fromFile = Configuration.fromFile;

const responseStub = sinon.stub().throws({code: "ENOENT"});
promisifyStub.callsFake(() => responseStub);

const config = await fromFile("/non-existing/path/.ui5rc");

t.is(config instanceof Configuration, true, "Created a default configuration");
t.is(config.getMavenSnapshotEndpointUrl(), undefined, "Dafault settings");
});

test.serial("fromFile: throws", async (t) => {
const fromFile = t.context.Configuration.fromFile;
const {promisifyStub, sinon} = t.context;

const responseStub = sinon.stub().throws(new Error("Error"));
promisifyStub.callsFake(() => responseStub);

await t.throwsAsync(fromFile("/non-existing/path/.ui5rc"), {
message: "Error"
});
});

test.serial("toFile", async (t) => {
const {promisifyStub, sinon, Configuration} = t.context;
const toFile = Configuration.toFile;

const writeStub = sinon.stub().resolves();
promisifyStub.callsFake(() => writeStub);

const config = new Configuration({mavenSnapshotEndpointUrl: "https://registry.corp/vendor/build-snapshots/"});
await toFile(config, "/path/to/save/.ui5rc");

t.deepEqual(
writeStub.getCall(0).args,
["/path/to/save/.ui5rc", JSON.stringify(config.toJSON())],
"Write config to path"
);
});
3 changes: 2 additions & 1 deletion test/lib/package-exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ test("export of package.json", (t) => {
// Check number of definied exports
test("check number of exports", (t) => {
const packageJson = require("@ui5/project/package.json");
t.is(Object.keys(packageJson.exports).length, 11);
t.is(Object.keys(packageJson.exports).length, 12);
});

// Public API contract (exported modules)
[
"config/Configuration",
"specifications/Specification",
"specifications/SpecificationVersion",
"ui5Framework/Openui5Resolver",
Expand Down
2 changes: 1 addition & 1 deletion test/lib/ui5framework/Sapui5MavenSnapshotResolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ test.serial("_resolveSnapshotEndpointUrlFromMaven", async (t) => {
"Using Maven snapshot endpoint URL resolved from Maven configuration file: /build-snapshots/");
t.is(loggerInfo.getCall(1).args[0],
"Consider persisting this choice by executing the following command: " +
"ui5 config set snapshotEndpointUrl /build-snapshots/");
"ui5 config set mavenSnapshotEndpointUrl /build-snapshots/");
});

test.serial("_resolveSnapshotEndpointUrlFromMaven fails", async (t) => {
Expand Down

0 comments on commit fd37cef

Please sign in to comment.