Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Enhance cli command line #107

Merged
merged 12 commits into from
Jul 5, 2023
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,43 @@ yarn run dev

Open `localhost:8080` on your browser

### Command line
The GBFS validator can be used a command line. Find below the steps for using cli feature:

- Download or clone this repository
```shell
git clone https://github.com/fluctuo/gbfs-validator.git
cd gbfs-validator
```
- Executes the cli script using one of the listed below options

```shell
./gbfs-validator/cli.js -u {http_address_of_gbfs_dataset} -s {local_path_to_output_report_file}
```
or
```shell
node ./gbfs-validator/cli.js -u {http_address_of_gbfs_dataset} -s {local_path_to_output_report_file}
```

## CLI help parameter
To get the list of supported paramters
```shell
./gbfs-validator/cli.js --help
```

## Usage description and supported parameters
```
Usage: cli [OPTIONS]...

Options:
-v, --version output the version number
-u, --url <feed_url> URL of the GBFS feed
-vb, --verbose Verbose mode prints debugging console logs
-s, --save-report <report_path> Local path to output report file
-pr, --print-report <yes_no> Print report to standard output (choices: "yes", "no", default: "yes")
Copy link
Contributor

@richfab richfab Jul 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Only if you accept the suggestion to use -p for consistency with -s)

Suggested change
-pr, --print-report <yes_no> Print report to standard output (choices: "yes", "no", default: "yes")
-p, --print-report <yes_no> Print report to standard output (choices: "yes", "no", default: "yes")

-h, --help display help for command
```

## Projects based on this validator

[transport.data.gouv.fr GBFS validator tool](https://transport.data.gouv.fr/validation?type=gbfs) - Tool displaying interactive geofencing, station, and vehicle maps, the validation results, and metadata of GBFS feeds.
Expand Down
94 changes: 53 additions & 41 deletions gbfs-validator/__test__/cli.test.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,69 @@
let path = require('path')
let exec = require('node:child_process').exec

const serverOpts = {
port: 0,
host: '127.0.0.1',
}

describe('cli', () => {
describe('without arguments', () => {
test('should show help without required url', async () => {
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {})
const mockConsoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {})

expect(() => {
require('../cli.js')
}).toThrow('Missing URL')

expect(mockExit).toHaveBeenCalledWith(1)
})
const cliExec = (args) => {
const command = `${path.resolve(`${__dirname}/../cli.js`)} ${args.join(' ')}`
return new Promise(resolve => {
exec(command,
Dismissed Show dismissed Hide dismissed
// Setting exit override to allow program to exit simplifying unit testing
{ env: { ...process.env, 'EXIT_OVERRIDE': 'true' } },
(error, stdout, stderr) => {
resolve({
code: error && error.code ? error.code : 0,
error,
stdout,
stderr
})
})
})
}

describe('with arguments', () => {
let gbfsFeedServer
describe('cli', () => {

beforeAll(async () => {
gbfsFeedServer = require('./fixtures/server')()
let gbfsFeedServer
let feedUrl

await gbfsFeedServer.listen(serverOpts)
beforeAll(async () => {
gbfsFeedServer = require('./fixtures/server')()
await gbfsFeedServer.listen(serverOpts)
feedUrl = `http://${gbfsFeedServer.server.address().address}:${gbfsFeedServer.server.address().port}/gbfs.json`
})

return gbfsFeedServer
})
afterAll(() => {
return gbfsFeedServer.close()
})

afterAll(() => {
return gbfsFeedServer.close()
})
test('should show an error if url parameter is not set', async () => {
const result = await cliExec([])
expect(result.code).toBe(1)
expect(result.error.message).toContain('error: required option \'-u, --url <feed_url>\' not specified')
})

test('should run cli', async () => {
const url = `http://${gbfsFeedServer.server.address().address}:${
gbfsFeedServer.server.address().port
}`
test('should success and print the report when url parameter set and -pr is set as default', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Only if you accept the suggestion to use -p for consistency with -s)

Suggested change
test('should success and print the report when url parameter set and -pr is set as default', async () => {
test('should success and print the report when url parameter set and -p is set as default', async () => {

const result = await cliExec([`-u`, `${feedUrl}`])
expect(result.code).toBe(0)
expect(result.stdout).toContain('summary:')
})

process.argv[2] = url
test('should show an error if pr parameter is set to `no` and -s is not set', async () => {
const result = await cliExec([`-u ${feedUrl}`, '-pr no'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Only if you accept the suggestion to use -p for consistency with -s)

Suggested change
const result = await cliExec([`-u ${feedUrl}`, '-pr no'])
const result = await cliExec([`-u ${feedUrl}`, '-p no'])

expect(result.code).toBe(1)
expect(result.stdout).toContain('Please set at least one of the following options: --save-report or --print-report')
})

const cli = await require('../cli.js')
test('should success when paramters url and save report has valid values and print report is set to no', async () => {
const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-pr no'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Only if you accept the suggestion to use -p for consistency with -s)

Suggested change
const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-pr no'])
const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-p no'])

expect(result.code).toBe(0)
})

expect(cli).toMatchObject({
summary: expect.objectContaining({
version: { detected: '2.2', validated: '2.2' },
hasErrors: true,
validatorVersion: '1.0.0',
errorsCount: 1
}),
files: expect.any(Array)
})
})
test('should success and print report when paramters url and save report are valid and print report is set to yes', async () => {
const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-pr yes'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Only if you accept the suggestion to use -p for consistency with -s)

Suggested change
const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-pr yes'])
const result = await cliExec([`-u ${feedUrl}`, '-s /dev/null', '-p yes'])

expect(result.code).toBe(0)
expect(result.stdout).toContain('summary:')
})
})
})
98 changes: 88 additions & 10 deletions gbfs-validator/cli.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,95 @@
#!/usr/bin/env node

const { inspect } = require('util')
const GBFS = require('./')
const commander = require('commander')
const fs = require('fs')
const fsPath = require('path')
const GBFS = require('./gbfs')
const pjson = require('./package.json')

getFeedValidationReport = async (url) => {
const gbfs = new GBFS(url)
return gbfs.validation()
}

const isExitOverrided = () => (process.env.EXIT_OVERRIDE === 'true')

if (!process.argv[2]) {
console.error('Usage: gbfs-validator GBFS_URL')
process.exit(1)
const printingReport = (options) => (options.printReport === 'yes')

const exitProcess = (code) => {
if (!isExitOverrided && code === 1) {
process.exit(code)
}
process.exit(0)
}

const gbfs = new GBFS(process.argv[2])
parseOptions = () => {
commander
.version(pjson.version, '-v, --version')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always 1.0.0 because we don't use it right now.
We return the git hash instead https://github.com/MobilityData/gbfs-validator/blob/master/gbfs-validator/gbfs.js#L3

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is in preparation for publishing an npm package. In the case of the command line, if we would like to include the commit hash, more work is needed to make available the hash information on local computers.

.usage('[OPTIONS]...')
.requiredOption('-u, --url <feed_url>', 'URL of the GBFS feed')
.option('-vb, --verbose', 'Verbose mode prints debugging console logs')
.option('-s, --save-report <report_path>', 'Local path to output report file')
.addOption(new commander.Option('-pr, --print-report <yes_no>', 'Print report to standard output').default('yes').choices(['yes', 'no']))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with -s, could we use one letter -p for printing?

Suggested change
.addOption(new commander.Option('-pr, --print-report <yes_no>', 'Print report to standard output').default('yes').choices(['yes', 'no']))
.addOption(new commander.Option('-p, --print-report <yes_no>', 'Print report to standard output').default('yes').choices(['yes', 'no']))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I connected with @richfab, and we couldn't find any strong feelings either way. Let's keep it as it is for simplicity; in the future, more options will be added, and the short argument name will be always a point of conflict.


// Supporting friendly unit testing and possible CI integrations
// The process throw an exception on parsing error in addition to the parsing error
if (isExitOverrided()) {
commander.exitOverride()
}
return commander.parse(process.argv).opts()
}

const saveReport = (report, filePath) => {
const dirname = fsPath.dirname(filePath);
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(report))
}

const processFeedValidation = async (options) => {
if (options.verbose) {
console.log("Started GBFS validation with options: \n " + inspect(options, { depth: null, colors: true }))
}
try {
const report = await getFeedValidationReport(options.url)
if (printingReport(options)) {
console.log(inspect(report, { depth: null, colors: true }))
}
if (options.saveReport) {
saveReport(report, options.saveReport)
}
} catch (error) {
console.error(`Critical error while validating GBFS feed => ${error}`)
exitProcess(1);
}
}

const validate = () => {
const options = parseOptions()
if (!options.saveReport && !printingReport(options)) {
console.log('Please set at least one of the following options: --save-report or --print-report')
commander.help()
exitProcess(1)
}

processFeedValidation(options).then(
() => {
if (options.verbose) {
console.log("Validation completed")
}
}
)
}

if (require.main === module) {
gbfs
.validation()
.then(result => console.log(inspect(result, { depth: null, colors: true })))
validate()
} else {
module.exports = gbfs.validation()
}
module.exports = {
validate,
processFeedValidation,
saveReport,
getFeedValidationReport,
}
}
1 change: 1 addition & 0 deletions gbfs-validator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"ajv": "^8.9.0",
"ajv-errors": "^3.0.0",
"ajv-formats": "^2.1.1",
"commander": "^11.0.0",
"fast-json-patch": "^3.1.0",
"got": "^11.8.2",
"json-merge-patch": "^1.0.2"
Expand Down
Loading
Loading