diff --git a/bids-validator/build.ts b/bids-validator/build.ts index 26fc6b7ea..92cacfd11 100755 --- a/bids-validator/build.ts +++ b/bids-validator/build.ts @@ -27,7 +27,7 @@ const flags = parse(Deno.args, { const version = await getVersion() -let versionPlugin = { +const versionPlugin = { name: 'version', setup(build: esbuild.PluginBuild) { build.onResolve({ filter: /\.git-meta\.json/ }, (args) => ({ @@ -46,7 +46,7 @@ const result = await esbuild.build({ format: 'esm', entryPoints: [MAIN_ENTRY, CLI_ENTRY], bundle: true, - outdir: path.join('dist','validator'), + outdir: path.join('dist', 'validator'), minify: flags.minify, target: ['chrome109', 'firefox109', 'safari16'], plugins: [ diff --git a/bids-validator/deno.json b/bids-validator/deno.json index a5c96b9f1..32abd0666 100644 --- a/bids-validator/deno.json +++ b/bids-validator/deno.json @@ -27,20 +27,21 @@ ] }, "imports": { + "@ajv": "npm:ajv@8.16.0", + "@bids/schema": "jsr:@bids/schema@0.11.2-dev.2+7f1f6737", + "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.5", + "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.5", + "@hed/validator": "npm:hed-validator@3.15.4", + "@ignore": "npm:ignore@5.3.2", + "@libs/xml": "jsr:@libs/xml@5.4.13", + "@mango/nifti": "npm:nifti-reader-js@0.6.8", "@std/assert": "jsr:@std/assert@1.0.2", "@std/fmt": "jsr:@std/fmt@1.0.0", "@std/fs": "jsr:@std/fs@1.0.1", "@std/io": "jsr:@std/io@0.224.4", "@std/log": "jsr:@std/log@0.224.5", "@std/path": "jsr:@std/path@1.0.2", - "@ajv": "npm:ajv@8.16.0", - "@bids/schema": "jsr:@bids/schema@0.11.1-dev.4+a73c1b06", - "@cliffy/table": "jsr:@cliffy/table@1.0.0-rc.5", - "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.5", - "@hed/validator": "npm:hed-validator@3.15.4", - "@ignore": "npm:ignore@5.3.2", - "@mango/nifti": "npm:nifti-reader-js@0.6.8", - "@libs/xml": "jsr:@libs/xml@5.4.13" + "@std/yaml": "jsr:@std/yaml@^1.0.4" }, "tasks": { "test": "deno test -A src/tests/" diff --git a/bids-validator/src/issues/list.ts b/bids-validator/src/issues/list.ts index 0f3336094..779a7cdef 100644 --- a/bids-validator/src/issues/list.ts +++ b/bids-validator/src/issues/list.ts @@ -162,6 +162,20 @@ export const bidsIssues: IssueDefinitionRecord = { severity: 'error', reason: 'A json sidecar file was found without a corresponding data file', }, + BLACKLISTED_MODALITY: { + severity: 'error', + reason: 'The modality in this file is blacklisted through validator configuration.', + }, + CITATION_CFF_VALIDATION_ERROR: { + severity: 'error', + reason: + "The file does not pass validation using the citation.cff standard's schema." + + 'https://github.com/citation-file-format/citation-file-format/blob/main/schema-guide.md' + }, + FILE_READ: { + severity: 'error', + reason: 'We were unable to read this file.' + } } const hedIssues: IssueDefinitionRecord = { @@ -191,10 +205,6 @@ const hedIssues: IssueDefinitionRecord = { reason: "You should define 'HEDVersion' for this file. If you don't provide this information, the HED validation will use the latest version available.", }, - BLACKLISTED_MODALITY: { - severity: 'error', - reason: 'The modality in this file is blacklisted through validator configuration.', - }, } export const hedOldToNewLookup: Record> = { diff --git a/bids-validator/src/setup/loadSchema.test.ts b/bids-validator/src/setup/loadSchema.test.ts index 896040a75..1dc9b2e63 100644 --- a/bids-validator/src/setup/loadSchema.test.ts +++ b/bids-validator/src/setup/loadSchema.test.ts @@ -1,7 +1,7 @@ import { assert, assertObjectMatch } from '@std/assert' import { loadSchema } from './loadSchema.ts' -Deno.test('schema yaml loader', async (t) => { +Deno.test('schema loader', async (t) => { await t.step('reads in top level files document', async () => { const schemaDefs = await loadSchema() // Look for some stable fields in top level files diff --git a/bids-validator/src/validators/bids.ts b/bids-validator/src/validators/bids.ts index 408943e63..5b0d96510 100644 --- a/bids-validator/src/validators/bids.ts +++ b/bids-validator/src/validators/bids.ts @@ -17,6 +17,7 @@ import { sidecarWithoutDatafile, unusedStimulus } from './internal/unusedFile.ts import { type BIDSContext, BIDSContextDataset } from '../schema/context.ts' import type { parseOptions } from '../setup/options.ts' import { hedValidate } from './hed.ts' +import { citationValidate } from './citation.ts' /** * Ordering of checks to apply @@ -32,6 +33,7 @@ const perContextChecks: ContextCheckFunction[] = [ const perDSChecks: DSCheckFunction[] = [ unusedStimulus, sidecarWithoutDatafile, + citationValidate, ] /** diff --git a/bids-validator/src/validators/citation.test.ts b/bids-validator/src/validators/citation.test.ts new file mode 100644 index 000000000..16bcfbcba --- /dev/null +++ b/bids-validator/src/validators/citation.test.ts @@ -0,0 +1,27 @@ +import { assert } from '@std/assert' +import { pathsToTree } from '../files/filetree.ts' +import { BIDSFileDeno } from '../files/deno.ts' +import { citationValidate } from './citation.ts' +import { BIDSContextDataset } from '../schema/context.ts' +import { GenericSchema } from '../types/schema.ts' +import { loadSchema } from '../setup/loadSchema.ts' + +Deno.test('citation validation', async (t) => { + const schema = await loadSchema() + await t.step('no errors on the good citation.cff', async () => { + const tree = pathsToTree(['CITATION.cff']) + const dsContext = new BIDSContextDataset({ tree }) + const file = new BIDSFileDeno('tests/data/citation', 'good.cff') + tree.files[0].text = () => file.text() + await citationValidate({} as GenericSchema, dsContext) + assert(dsContext.issues.size === 0) + }) + await t.step('An error on the bad citation.cff', async () => { + const tree = pathsToTree(['CITATION.cff']) + const dsContext = new BIDSContextDataset({ tree }) + const file = new BIDSFileDeno('tests/data/citation', 'bad.cff') + tree.files[0].text = () => file.text() + await citationValidate({} as GenericSchema, dsContext) + assert(dsContext.issues.get({ code: 'CITATION_CFF_VALIDATION_ERROR' }).length === 1) + }) +}) diff --git a/bids-validator/src/validators/citation.ts b/bids-validator/src/validators/citation.ts new file mode 100644 index 000000000..905ef80ef --- /dev/null +++ b/bids-validator/src/validators/citation.ts @@ -0,0 +1,39 @@ +import type { GenericSchema } from '../types/schema.ts' +import type { BIDSFile, FileTree } from '../types/filetree.ts' +import type { BIDSContextDataset } from '../schema/context.ts' +import { schema as citationSchema } from '@bids/schema/citation' +import { compile } from './json.ts' +import type { DefinedError } from '@ajv' +import { parse } from '@std/yaml' + +const citationFilename = 'CITATION.cff' + +export async function citationValidate( + schema: GenericSchema, + dsContext: BIDSContextDataset, +) { + const citationFile = dsContext.tree.get(citationFilename) + if (!citationFile || 'directories' in citationFile) return + let citation: unknown = {} + try { + citation = parse(await citationFile.text()) + } catch (error) { + dsContext.issues.add({ + code: 'FILE_READ', + issueMessage: `Error from attempted read of file:\n${error}`, + location: citationFilename, + }) + return + } + const validate = compile(citationSchema) + if (!validate(citation)) { + for (const err of validate.errors as DefinedError[]) { + dsContext.issues.add({ + code: 'CITATION_CFF_VALIDATION_ERROR', + subCode: err['instancePath'], + issueMessage: err['message'], + location: citationFilename, + }) + } + } +} diff --git a/bids-validator/tests/data/citation/bad.cff b/bids-validator/tests/data/citation/bad.cff new file mode 100644 index 000000000..71e85ecfc --- /dev/null +++ b/bids-validator/tests/data/citation/bad.cff @@ -0,0 +1,15 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it using these metadata. +title: My Research Software +abstract: This is my awesome research software. It does many things. +version: 0.11.2 +date-released: "2021-07-18" +identifiers: + - description: This is the collection of archived snapshots of all versions of My Research Software + type: doi + value: "10.5281/zenodo.123456" + - description: This is the archived snapshot of version 0.11.2 of My Research Software + type: doi + value: "10.5281/zenodo.123457" +license: Apache-2.0 +repository-code: "https://github.com/citation-file-format/my-research-software" diff --git a/bids-validator/tests/data/citation/good.cff b/bids-validator/tests/data/citation/good.cff new file mode 100644 index 000000000..f935b0fef --- /dev/null +++ b/bids-validator/tests/data/citation/good.cff @@ -0,0 +1,20 @@ +cff-version: 1.2.0 +message: If you use this software, please cite it using these metadata. +title: My Research Software +abstract: This is my awesome research software. It does many things. +authors: + - family-names: Druskat + given-names: Stephan + orcid: "https://orcid.org/1234-5678-9101-1121" + - name: "The Research Software project" +version: 0.11.2 +date-released: "2021-07-18" +identifiers: + - description: This is the collection of archived snapshots of all versions of My Research Software + type: doi + value: "10.5281/zenodo.123456" + - description: This is the archived snapshot of version 0.11.2 of My Research Software + type: doi + value: "10.5281/zenodo.123457" +license: Apache-2.0 +repository-code: "https://github.com/citation-file-format/my-research-software"