diff --git a/.gitignore b/.gitignore index 31408d13..5971f599 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ generated/ .idea/ *.iml + +# VSCode .vscode/ +vscode_wspace.code-workspace diff --git a/README.md b/README.md index bd958582..76a33417 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,13 @@ lit-artifact-generator --help # How to run +1. To generate source code from a vocabulary or from a config YAML file: ```shell -node index.js --inputResources +node index.js generate --inputResources +``` +2. To create an initial YAML file that should be edited manually +```shell +node index.js init ``` The output is a Node Module containing a Javascript file with constants defined for the RDF terms found in the vocabulary specified by the 'inputResources' flag. This module is located inside the **./generated** folder by default. diff --git a/index.js b/index.js index fad53af7..7d246fe9 100755 --- a/index.js +++ b/index.js @@ -8,51 +8,123 @@ require('mock-local-storage'); const logger = require('debug')('lit-artifact-generator:index'); +const debug = require('debug'); const yargs = require('yargs'); const App = require('./src/App'); console.log(`Command: [${process.argv.join(' ')}]`); const yargsConfig = yargs - .alias('i', 'inputResources') - .array('inputResources') - .describe( - 'inputResources', - "One or more ontology resources (i.e. local RDF files, or HTTP URI's) used to generate source-code artifacts representing the contained vocabulary terms." + .command( + 'generate', + 'generate code artifacts from RDF', + yargs => + yargs + .alias('i', 'inputResources') + .array('inputResources') + .describe( + 'inputResources', + "One or more ontology resources (i.e. local RDF files, or HTTP URI's) used to generate source-code artifacts representing the contained vocabulary terms." + ) + + .alias('l', 'vocabListFile') + .describe( + 'vocabListFile', + 'Name of a YAML file providing a list of individual vocabs to bundle together into a single artifact (or potentially multiple artifacts for multiple programming languages).' + ) + + .alias('lv', 'litVocabTermVersion') + .describe('litVocabTermVersion', 'The version of the LIT Vocab Term to depend on.') + .default('litVocabTermVersion', '^0.1.0') + + .alias('in', 'runNpmInstall') + .boolean('runNpmInstall') + .describe( + 'runNpmInstall', + 'If set will attempt to NPM install the generated artifact from within the output directory.' + ) + .default('runNpmInstall', false) + + .alias('p', 'runNpmPublish') + .boolean('runNpmPublish') + .describe('runNpmPublish', 'If set will attempt to publish to the configured NPM registry.') + .default('runNpmPublish', false) + + .alias('b', 'bumpVersion') + .describe( + 'bumpVersion', + 'Bump up the semantic version of the artifact from the currently published version.' + ) + .choices('bumpVersion', ['patch', 'minor', 'major']) + + .alias('vtf', 'vocabTermsFrom') + .describe('vocabTermsFrom', 'Generates Vocab Terms from only the specified ontology file.') + + .alias('av', 'artifactVersion') + .describe('artifactVersion', 'The version of the artifact(s) to be generated.') + .default('artifactVersion', '0.0.1') + + .alias('at', 'artifactType') + .describe('artifactType', 'The artifact type that will be generated.') + .choices('artifactType', ['nodejs', 'java']) // Add to this when other languages are supported. + .default('artifactType', 'nodejs') + + .alias('mnp', 'moduleNamePrefix') + .describe('moduleNamePrefix', 'A prefix for the name of the output module') + .default('moduleNamePrefix', '@lit/generated-vocab-') + + .alias('nr', 'npmRegistry') + .describe('npmRegistry', 'The NPM Registry where artifacts will be published') + .default('npmRegistry', 'https://verdaccio.inrupt.com') + + .alias('w', 'runWidoco') + .boolean('runWidoco') + .describe( + 'runWidoco', + 'If set will run Widoco to generate documentation for this vocabulary.' + ) + + .alias('s', 'supportBundling') + .boolean('supportBundling') + .describe( + 'supportBundling', + 'If set will use bundling support within generated artifact (currently supports Webpack only).' + ) + .default('supportBundling', true) + // Can't provide an explicit version, and then also request a version bump! + .conflicts('artifactVersion', 'bumpVersion') + + // Must provide either an input vocab file, or a file containing a list of vocab files (but how can we demand at + // least one of these two...?) + .conflicts('inputResources', 'vocabListFile') + .strict(), + argv => { + if (!argv.inputResources && !argv.vocabListFile) { + // this.yargsConfig.showHelp(); + logger(argv.help); + debug.enable('lit-artifact-generator:*'); + throw new Error( + "You must provide input, either a single vocabulary using '--inputResources' (e.g. a local RDF file, or a URL that resolves to an RDF vocabulary), or a YAML file using '--vocabListFile' listing multiple vocabularies." + ); + } + runGeneration(argv); + } ) - - .alias('l', 'vocabListFile') - .describe( - 'vocabListFile', - 'Name of a YAML file providing a list of individual vocabs to bundle together into a single artifact (or potentially multiple artifacts for multiple programming languages).' - ) - - .alias('lv', 'litVocabTermVersion') - .describe('litVocabTermVersion', 'The version of the LIT Vocab Term to depend on.') - .default('litVocabTermVersion', '^0.1.0') - - .alias('o', 'outputDirectory') - .describe('outputDirectory', 'The output directory for the generated artifacts.') - .default('outputDirectory', './generated') - - .alias('in', 'runNpmInstall') - .boolean('runNpmInstall') - .describe( - 'runNpmInstall', - 'If set will attempt to NPM install the generated artifact from within the output directory.' + .command( + 'init', + 'initializes a config file used for generation', + yargs => yargs, + argv => { + runInitialization(argv); + } ) - .default('runNpmInstall', false) - - .alias('p', 'runNpmPublish') - .boolean('runNpmPublish') - .describe('runNpmPublish', 'If set will attempt to publish to the configured NPM registry.') - .default('runNpmPublish', false) - - .alias('b', 'bumpVersion') + // The following options are shared between the different commands + .alias('q', 'quiet') + .boolean('quiet') .describe( - 'bumpVersion', - 'Bump up the semantic version of the artifact from the currently published version.' + 'quiet', + `If set will not display logging output to console (but you can still use DEBUG environment variable, set to 'lit-artifact-generator:*').` ) - .choices('bumpVersion', ['patch', 'minor', 'major']) + .default('quiet', false) .alias('np', 'noprompt') .boolean('noprompt') @@ -62,61 +134,54 @@ const yargsConfig = yargs ) .default('noprompt', false) - .alias('q', 'quiet') - .boolean('quiet') - .describe( - 'quiet', - `If set will not display logging output to console (but you can still use DEBUG environment variable, set to 'lit-artifact-generator:*').` - ) - .default('quiet', false) - - .alias('vtf', 'vocabTermsFrom') - .describe('vocabTermsFrom', 'Generates Vocab Terms from only the specified ontology file.') - - .alias('av', 'artifactVersion') - .describe('artifactVersion', 'The version of the artifact(s) to be generated.') - .default('artifactVersion', '0.0.1') - - .alias('at', 'artifactType') - .describe('artifactType', 'The artifact type that will be generated.') - .choices('artifactType', ['nodejs', 'java']) // Add to this when other languages are supported. - .default('artifactType', 'nodejs') - - .alias('mnp', 'moduleNamePrefix') - .describe('moduleNamePrefix', 'A prefix for the name of the output module') - .default('moduleNamePrefix', '@lit/generated-vocab-') - - .alias('nr', 'npmRegistry') - .describe('npmRegistry', 'The NPM Registry where artifacts will be published') - .default('npmRegistry', 'https://verdaccio.inrupt.com') - - .alias('w', 'runWidoco') - .boolean('runWidoco') - .describe('runWidoco', 'If set will run Widoco to generate documentation for this vocabulary.') + .alias('o', 'outputDirectory') + .describe('outputDirectory', 'The output directory for the generated artifacts.') + .default('outputDirectory', './generated') - .alias('s', 'supportBundling') - .boolean('supportBundling') - .describe( - 'supportBundling', - 'If set will use bundling support within generated artifact (currently supports Webpack only).' - ) - .default('supportBundling', true) - - // Can't provide an explicit version, and then also request a version bump! - .conflicts('artifactVersion', 'bumpVersion') - - // Must provide either an input vocab file, or a file containing a list of vocab files (but how can we demand at - // least one of these two...?) - .conflicts('inputResources', 'vocabListFile') - .strict(); - -new App(yargsConfig) - .run() - .then(data => { - logger(`\nGeneration process successful to directory [${data.outputDirectory}]!`); - process.exit(0); - }) - .catch(error => { - logger(`Generation process failed: [${error}]`); - process.exit(-1); - }); + .help().argv; + +function configureLog(argv) { + // Unless specifically told to be quiet (i.e. no logging output, although that + // will still be overridden by the DEBUG environment variable!), then + // determine if any generator-specific namespaces have been enabled. If they + // haven't been, then turn them all on, + if (!argv.quiet) { + // Retrieve all currently enabled debug namespaces (and then restore them!). + const namespaces = debug.disable(); + debug.enable(namespaces); + + // Unless our generator's debug logging has been explicitly configured, turn + // all debugging on. + if (namespaces.indexOf('lit-artifact-generator') === -1) { + debug.enable('lit-artifact-generator:*'); + } + } +} + +function runGeneration(argv) { + configureLog(argv); + new App(argv) + .run() + .then(data => { + logger(`\nGeneration process successful to directory [${data.outputDirectory}]!`); + process.exit(0); + }) + .catch(error => { + logger(`Generation process failed: [${error}]`); + process.exit(-1); + }); +} + +function runInitialization(argv) { + configureLog(argv); + new App(argv) + .init() + .then(data => { + logger(`\nSuccessfully initialized config file [${data}]`); + process.exit(0); + }) + .catch(error => { + logger(`Generation process failed: [${error}]`); + process.exit(-1); + }); +} diff --git a/src/App.js b/src/App.js index 5634d889..b319d54b 100755 --- a/src/App.js +++ b/src/App.js @@ -1,44 +1,29 @@ -const debug = require('debug'); +const path = require('path'); +const moment = require('moment'); + const ArtifactGenerator = require('./generator/ArtifactGenerator'); const CommandLine = require('./CommandLine'); +const FileGenerator = require('./generator/FileGenerator'); +const packageDotJson = require('../package.json'); + +const DEFAULT_CONFIG_TEMPLATE_PATH = '../../templates/initial-config.hbs'; +const DEFAULT_CONFIG_NAME = 'lit-vocab.yml'; module.exports = class App { - constructor(yargsConfig) { - if (!yargsConfig) { + constructor(argv) { + if (!argv) { throw new Error('Application must be initialised with a configuration - none was provided.'); } - this.yargsConfig = yargsConfig; + this.argv = argv; + + // Extend the received arguments with contextual data + this.argv.generatedTimestamp = moment().format('LLLL'); + this.argv.generatorName = packageDotJson.name; + this.argv.generatorVersion = packageDotJson.version; } async run() { - // Process the YARGS config data... - this.argv = this.yargsConfig.argv; - - if (!this.argv.inputResources && !this.argv.vocabListFile) { - this.yargsConfig.showHelp(); - debug.enable('lit-artifact-generator:*'); - throw new Error( - "You must provide input, either a single vocabulary using '--inputResources' (e.g. a local RDF file, or a URL that resolves to an RDF vocabulary), or a YAML file using '--vocabListFile' listing multiple vocabularies." - ); - } - - // Unless specifically told to be quiet (i.e. no logging output, although that - // will still be overridden by the DEBUG environment variable!), then - // determine if any generator-specific namespaces have been enabled. If they - // haven't been, then turn them all on, - if (!this.argv.quiet) { - // Retrieve all currently enabled debug namespaces (and then restore them!). - const namespaces = debug.disable(); - debug.enable(namespaces); - - // Unless our generator's debug logging has been explicitly configured, turn - // all debugging on. - if (namespaces.indexOf('lit-artifact-generator') === -1) { - debug.enable('lit-artifact-generator:*'); - } - } - const artifactGenerator = new ArtifactGenerator(this.argv, CommandLine.askForArtifactInfo); return artifactGenerator @@ -48,4 +33,17 @@ module.exports = class App { .then(CommandLine.askForArtifactToBeNpmPublished) .then(CommandLine.askForArtifactToBeDocumented); } + + async init() { + return new Promise(resolve => { + const targetPath = path.join(this.argv.outputDirectory, DEFAULT_CONFIG_NAME); + + FileGenerator.createDirectory(this.argv.outputDirectory); + // This method is synchronous, so the wrapping promise just provices uniformity + // with the other methods of the class + + FileGenerator.createFileFromTemplate(DEFAULT_CONFIG_TEMPLATE_PATH, this.argv, targetPath); + resolve(targetPath); + }); + } }; diff --git a/src/App.test.js b/src/App.test.js index c9e3109f..bfa8fdf9 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,5 +1,7 @@ require('mock-local-storage'); const debug = require('debug'); +const fs = require('fs'); +const path = require('path'); const App = require('./App'); const ArtifactGenerator = require('./generator/ArtifactGenerator'); @@ -26,15 +28,6 @@ describe('App tests', () => { expect(() => new App(config)).not.toThrow(); }); - it('should fail with missing input', async () => { - const config = { - argv: {}, - showHelp: () => {}, - }; - - await expect(new App(config).run()).rejects.toThrow('You must provide input'); - }); - describe('Testing mocked generator...', () => { it('should pass through in non-quiet mode (with DEBUG setting too)', async () => { debug.enable('lit-artifact-generator:*'); @@ -69,5 +62,15 @@ describe('App tests', () => { expect(mockedResponse.noprompt).toBe(true); expect(mockedResponse.stubbed).toBe(true); }); + + it('should generate a default file', async () => { + const directoryPath = path.join('.', '.tmp'); + const filePath = path.join(directoryPath, 'lit-vocab.yml'); + const argv = { outputDirectory: directoryPath, quiet: false, noprompt: true }; + await new App(argv).init(); + expect(fs.existsSync(filePath)).toBe(true); + fs.unlinkSync(filePath); + fs.rmdirSync(directoryPath); + }); }); }); diff --git a/src/generator/ArtifactGenerator.js b/src/generator/ArtifactGenerator.js index 6fcfee08..3868fe89 100644 --- a/src/generator/ArtifactGenerator.js +++ b/src/generator/ArtifactGenerator.js @@ -1,12 +1,10 @@ const fs = require('fs'); const del = require('del'); const yaml = require('js-yaml'); -const moment = require('moment'); const logger = require('debug')('lit-artifact-generator:VocabGenerator'); const FileGenerator = require('./FileGenerator'); const VocabGenerator = require('./VocabGenerator'); -const packageDotJson = require('../../package.json'); const ARTIFACT_DIRECTORY_ROOT = '/Generated'; const ARTIFACT_DIRECTORY_SOURCE_CODE = `${ARTIFACT_DIRECTORY_ROOT}/SourceCodeArtifacts`; @@ -23,10 +21,6 @@ class ArtifactGenerator { // This collection will be populated with the authors per generated vocab. this.artifactData.authorSet = new Set(); - this.artifactData.generatedTimestamp = moment().format('LLLL'); - this.artifactData.generatorName = packageDotJson.name; - this.artifactData.generatorVersion = packageDotJson.version; - // TODO: Just hard-coding for the moment (still investigating Webpack...) this.artifactData.versionWebpack = '^4.39.1'; this.artifactData.versionWebpackCli = '^3.3.6'; @@ -123,6 +117,15 @@ class ArtifactGenerator { // Provide access to our entire YAML data. this.artifactData.generationDetails = generationDetails; + // If the vocab list is non-existent or empty (e.g. after initialization), the generator + // cannot run. + if (!generationDetails.vocabList) { + throw new Error( + 'No vocabularies found: nothing to generate. ' + + `Please edit the YAML configuration file [${this.artifactData.vocabListFile}] to provide vocabularies to generate from.` + ); + } + // For each programming language artifact we generate, first clear out the destination directories. const directoryDeletionPromises = generationDetails.artifactToGenerate.map(artifactDetails => { return ArtifactGenerator.deleteDirectory( diff --git a/src/generator/ArtifactGenerator.test.js b/src/generator/ArtifactGenerator.test.js index dede45e6..66ef74ba 100644 --- a/src/generator/ArtifactGenerator.test.js +++ b/src/generator/ArtifactGenerator.test.js @@ -138,5 +138,25 @@ describe('Artifact Generator', () => { const packageOutput = fs.readFileSync(`${outputDirectoryJavascript}/package.json`).toString(); expect(packageOutput.indexOf('"devDependencies",')).toEqual(-1); }); + + it('should throw an error trying to generate from an empty vocab list', async () => { + const outputDirectory = 'test/generated/ArtifactGenerator/'; + del.sync([`${outputDirectory}/*`]); + + const configFile = 'empty-vocab-list.yml'; + const configPath = `./test/resources/vocabs/${configFile}`; + + const artifactGenerator = new ArtifactGenerator({ + vocabListFile: configPath, + outputDirectory, + artifactVersion: '1.0.0', + moduleNamePrefix: '@lit/generated-vocab-', + }); + // Test that the error message contains the expected explanation and file name + await expect(artifactGenerator.generate()).rejects.toThrow( + /^No vocabularies found/, + configFile + ); + }); }); }); diff --git a/templates/initial-config.hbs b/templates/initial-config.hbs new file mode 100644 index 00000000..d67375a4 --- /dev/null +++ b/templates/initial-config.hbs @@ -0,0 +1,64 @@ +# This configuration file can be used as an input by [{{generatorName}}] to generate +# code artifacts from RDF vocabularies. +# +# Generated by artifact generator [{{generatorName}}], version [{{generatorVersion}}] +# on '{{generatedTimestamp}}'. + +# The name is shared among all the artifacts +artifactName: exampleName + +artifactToGenerate: + - programmingLanguage: Java + artifactVersion: 0.1.0-SNAPSHOT + javaPackageName: com.example.java.packagename + litVocabTermVersion: 0.1.0-SNAPSHOT + artifactFolderName: Java + handlebarsTemplate: java-rdf4j.hbs + sourceFileExtension: java + # Currently we're just adding terms as they occur in vocabs, and not all possible keywords. + languageKeywordsToUnderscore: + - class # Defined in VCard. + - abstract # Defined in DCTerms. +# repository: +# - enabled: true +# type: repository +# id: nexus-releases +# url: https://nexus.mycompany.com/repository/maven-releases/ + + - programmingLanguage: Javascript + artifactVersion: 0.1.0 +# gitRepository: git@github.com:some_user/some_project.git + npmModuleScope: "@exampleScope/" + litVocabTermVersion: "^0.1.0" + artifactFolderName: Javascript + handlebarsTemplate: javascript-rdf-ext.hbs + sourceFileExtension: js +# repository: +# - enabled: true +# registry: http://localhost:4873/ + + +vocabList: +# # Example of an online vocabulary. +# # This option is used as a name for the vocabulary, e.g. EXAMPLE.java or EXAMPLE.js +# # If not provided, the generator will look for the vann:preferredNamespacePrefix property, +# # and otherwise will propose a default based on the domain name. +# - nameAndPrefixOverride: example +# description: Some vocabulary +# # The following is the list of IRI to read when building the artifact. +# inputResources: +# - https://example.org/ns +# # This option allow us provide a second vocabulary that can add new terms, provide translations +# # for existing terms' labels or comments, and that also only generates output for terms explicitly +# # defined in this vocabulary (i.e. to selectively choose terms from the input resource(s)) +# termSelectionFile: /path/to/file + +# # Example of a local vocabulary. +# - description: A vocabulary stored in a local file +# nameAndPrefixOverride: anotherExample +# inputResources: +# - /path/to/vocabulary +# # This option allow us provide a second vocabulary that can add new terms, provide translations +# # for existing terms' labels or comments, and that also only generates output for terms explicitly +# # defined in this vocabulary (i.e. to selectively choose terms from the input resource(s)) +# termSelectionFile: /path/to/file \ No newline at end of file diff --git a/test/resources/vocabs/empty-vocab-list.yml b/test/resources/vocabs/empty-vocab-list.yml new file mode 100644 index 00000000..a5543104 --- /dev/null +++ b/test/resources/vocabs/empty-vocab-list.yml @@ -0,0 +1,28 @@ +# +# This file contains a simple list of vocabularies that we bundle together to +# form the collective set vocabularies within a single artifact. +# +artifactName: generated-vocab-common-TEST + +artifactToGenerate: + - programmingLanguage: Java + artifactVersion: 3.2.1-SNAPSHOT + javaPackageName: com.inrupt.testing + litVocabTermVersion: "0.1.0-SNAPSHOT" + artifactFolderName: Java + handlebarsTemplate: java-rdf4j.hbs + sourceFileExtension: java + # Currently we're just adding terms as they occur in vocabs, and not all possible keywords. + languageKeywordsToUnderscore: + - class # Defined in VCard. + - abstract # Defined in DCTerms. + + - programmingLanguage: Javascript + artifactVersion: 10.11.12 + npmModuleScope: "@lit/" + litVocabTermVersion: "^1.0.10" + artifactFolderName: Javascript + handlebarsTemplate: javascript-rdf-ext.hbs + sourceFileExtension: js + +vocabList: \ No newline at end of file