diff --git a/cypress/integration/basics.js b/cypress/integration/basics.js index d30cbe2..15fe098 100644 --- a/cypress/integration/basics.js +++ b/cypress/integration/basics.js @@ -1,110 +1,110 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Tests the basic features of the application. */ "use strict"; describe('JSON Generation', function() { beforeEach(function() { /* Clear the session storage, as it is used to restore field data; * and we don't want a test to load data from the previous test. */ cy.window().then((win) => { win.sessionStorage.clear() }) cy.visit('./index.html'); }); it('works just from the software name', function() { cy.get('#name').type('My Test Software'); cy.get('#generateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", }); }); it('works just from all main fields', function() { cy.get('#name').type('My Test Software'); cy.get('#description').type('This is a\ngreat piece of software'); cy.get('#dateCreated').type('2019-10-02'); cy.get('#datePublished').type('2020-01-01'); cy.get('#license').type('AGPL-3.0'); cy.get('#generateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => JSON.parse(elem.text())) .should('deep.equal', { "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", }); }); }); describe('JSON Import', function() { it('works just from the software name', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); }); it('works just from all main fields', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", })) ); cy.get('#importCodemeta').click(); cy.get('#name').should('have.value', 'My Test Software'); cy.get('#description').should('have.value', 'This is a\ngreat piece of software'); cy.get('#dateCreated').should('have.value', '2019-10-02'); cy.get('#datePublished').should('have.value', '2020-01-01'); cy.get('#license').should('have.value', 'AGPL-3.0'); }); it('errors on invalid type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "foo", "name": "My Test Software", })) ); cy.get('#importCodemeta').click(); // Should still be imported as much as possible cy.get('#name').should('have.value', 'My Test Software'); // But must display an error - cy.get('#errorMessage').should('have.text', 'Wrong document type: must be SoftwareSourceCode or SoftwareApplication, not "foo"'); + cy.get('#errorMessage').should('have.text', 'Wrong document type: must be "SoftwareSourceCode" or "SoftwareApplication", not "foo"'); }); }); diff --git a/cypress/integration/validation.js b/cypress/integration/validation.js index 2c38dec..8bba327 100644 --- a/cypress/integration/validation.js +++ b/cypress/integration/validation.js @@ -1,429 +1,590 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Tests the basic features of the application. */ "use strict"; describe('Document validation', function() { it('accepts empty document', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", })) ); cy.get('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); }); it('accepts all main fields', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "https://spdx.org/licenses/AGPL-3.0", "dateCreated": "2019-10-02", "datePublished": "2020-01-01", "name": "My Test Software", "description": "This is a\ngreat piece of software", })) ); cy.get('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', ''); }); it('accepts anything in non-validated fields', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "programmingLanguage": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "programmingLanguage": 21, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "programmingLanguage": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on invalid type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "foo", "name": "My Test Software", })) ); cy.get('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); - cy.get('#errorMessage').should('have.text', 'Wrong document type: must be SoftwareSourceCode or SoftwareApplication, not "foo"'); + cy.get('#errorMessage').should('have.text', 'Wrong document type: must be "SoftwareSourceCode" or "SoftwareApplication", not "foo"'); }); it('errors on invalid field name', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "foobar": "baz", })) ); cy.get('#validateCodemeta').click(); cy.get('#name').should('have.value', ''); cy.get('#errorMessage').should('have.text', 'Unknown field "foobar".'); }); }); describe('URLs validation', function() { it('accepts valid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": "http://example.org/", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts empty list of URLs', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of valid URLs', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", "http://example.com/"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on invalid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"'); }); it('errors on non-string instead of URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"codeRepository" must be an URL (or a list of URLs), not: {}'); }); it('errors on list with an invalid URL at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", "foo"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"'); }); it('errors on list with an invalid URL at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", "foo"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', 'Invalid URL in field "codeRepository": "foo"'); }); it('errors on non-string in URL list', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "codeRepository": ["http://example.org/", {}], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"codeRepository" must be a list of URLs (or a single URL), but it contains: {}'); }); }); describe('Texts or URLs validation', function() { it('accepts valid Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts valid URL', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": "http://example.org/", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts empty list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": ["foo", "bar"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on non-string instead of Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a text/URL (or a list of texts/URLs), not: {}'); }); it('errors on list with an invalid Text at the beginning', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": [{}, "foo"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a list of texts/URLs (or a single text/URL), but it contains: {}'); }); it('errors on list with an invalid Text at the end', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "applicationCategory": ["foo", {}], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"applicationCategory" must be a list of texts/URLs (or a single text/URL), but it contains: {}'); }); }); describe('Text validation', function() { it('accepts valid Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on empty list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"description" must be text, not []'); }); it('errors on list of Texts', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": ["foo", "bar"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"description" must be text, not ["foo","bar"]'); }); it('errors on non-string instead of Text', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "description": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"description" must be text, not {}'); }); }); describe('Date validation', function() { it('accepts valid Date', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": "2020-03-18", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on empty list of Dates', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not []'); }); it('errors on list of Dates', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": ["2020-03-18", "2020-03-19"], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not ["2020-03-18","2020-03-19"]'); }); it('errors on non-string instead of Date', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date, not {}'); }); it('errors on non-Date string', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "dateCreated": "foo", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"dateCreated" must be a date in the format YYYY-MM-DD, not "foo"'); }); }); +describe('Person validation', function() { + it('accepts valid complete Person', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": { + "@type": "Person", + "@id": "http://example.org/~jdoe", + "name": "Jane Doe", + "givenName": "Jane", + "familyName": "Doe", + "email": "jdoe@example.org", + "affiliation": { + "@type": "Organization", + "@id": "http://example.org/", + } + } + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', ''); + }); + + it('errors on Person with missing type', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": { + }, + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person or Organization object(s) or an URI, but is missing a type/@type.'); + }); + + it('errors on Person with wrong type', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": { + "type": "SoftwareSourceCode", + }, + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', '"author" type must be "Person" or "Organization", not "SoftwareSourceCode"'); + }); + + it('errors on Person with unknown field', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": { + "type": "Person", + "foo": "bar", + }, + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "author".'); + }); + + it('errors on Person with invalid field', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": { + "type": "Person", + "email": 32, + }, + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', '"email" must be text, not 32'); + }); + + it('accepts list of valid Person', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": [ + { + "@type": "Person", + "@id": "http://example.org/~jadoe", + "givenName": "Jane", + "familyName": "Doe", + }, + { + "@type": "Person", + "@id": "http://example.org/~jodoe", + "givenName": "John", + "familyName": "Doe", + }, + ] + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', ''); + }); + + it('errors on list with invalid Person at the beginning', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": [ + { + "givenName": "Jane", + "familyName": "Doe", + }, + { + "@type": "Person", + "@id": "http://example.org/~jodoe", + "name": "John Doe", + "givenName": "John", + "familyName": "Doe", + }, + ] + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person or Organization object(s) or an URI, but is missing a type/@type.'); + }); + + it('errors on list with invalid Person at the end', function() { + cy.get('#codemetaText').then((elem) => + elem.text(JSON.stringify({ + "@context": "https://doi.org/10.5063/schema/codemeta-2.0", + "@type": "SoftwareSourceCode", + "author": [ + { + "@type": "Person", + "@id": "http://example.org/~jadoe", + "givenName": "Jane", + "familyName": "Doe", + }, + { + "givenName": "John", + "familyName": "Doe", + }, + ] + })) + ); + cy.get('#validateCodemeta').click(); + + cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Person or Organization object(s) or an URI, but is missing a type/@type.'); + }); +}); diff --git a/js/validation.js b/js/validation.js index d8fbb3d..5d649ab 100644 --- a/js/validation.js +++ b/js/validation.js @@ -1,464 +1,499 @@ /** * Copyright (C) 2020 The Software Heritage developers * See the AUTHORS file at the top-level directory of this distribution * License: GNU Affero General Public License version 3, or any later version * See top-level LICENSE file for more information */ /* * Reads a Codemeta file and shows human-friendly errors on it. * * This validator intentionaly does not use a schema, in order to show errors * that are easy to understand for users with no understanding of JSON-LD. */ function getDocumentType(doc) { // TODO: check there is at most one. // FIXME: is the last variant allowed? return doc["type"] || doc["@type"] || doc["codemeta:type"] } function isCompactTypeEqual(type, compactedType) { // FIXME: are all variants allowed? return (type == `${compactedType}` || type == `schema:${compactedType}` || type == `codemeta:${compactedType}` || type == `http://schema.org/${compactedType}` ); } function noValidation(fieldName, doc) { return true; } // Helper function to validate a field is either X or a list of X. function validateListOrSingle(fieldName, doc, validator) { if (Array.isArray(doc)) { return doc.every((subdoc) => validator(subdoc, true)); } else { return validator(doc, false); } } // Validates an URL or an array of URLs function validateUrls(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { if (typeof subdoc != 'string') { if (inList) { setError(`"${fieldName}" must be a list of URLs (or a single URL), but it contains: ${JSON.stringify(subdoc)}`); } else { setError(`"${fieldName}" must be an URL (or a list of URLs), not: ${JSON.stringify(subdoc)}`); } return false; } else { return validateUrl(fieldName, subdoc); } }); } // Validates a single URL function validateUrl(fieldName, doc) { if (!isUrl(doc)) { setError(`Invalid URL in field "${fieldName}": ${JSON.stringify(doc)}`) return false; } else { return true; } } // Validates a Text/URL or an array of Texts/URLs function validateTextsOrUrls(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { if (typeof subdoc != 'string') { if (inList) { setError(`"${fieldName}" must be a list of texts/URLs (or a single text/URL), but it contains: ${JSON.stringify(subdoc)}`); } else { setError(`"${fieldName}" must be a text/URL (or a list of texts/URLs), not: ${JSON.stringify(subdoc)}`); } return false; } else { return true; } }); } // Validates a Text or an array of Texts function validateTexts(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { if (typeof subdoc != 'string') { if (inList) { setError(`"${fieldName}" must be a list of texts (or a single text), but it contains: ${JSON.stringify(subdoc)}`); } else { setError(`"${fieldName}" must be a text (or a list of texts), not: ${JSON.stringify(subdoc)}`); } return false; } else { return true; } }); } // Validates a single Text function validateText(fieldName, doc) { if (typeof doc != 'string') { setError(`"${fieldName}" must be text, not ${JSON.stringify(doc)}`); return false; } else { return true; } } // Validates a Number or list of Number function validateNumbers(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { if (typeof subdoc != 'number') { if (inList) { setError(`"${fieldName}" must be an array of numbers (or a single number), but contains: ${JSON.stringify(subdoc)}`); } else { setError(`"${fieldName}" must be a number or an array of numbers, not: ${JSON.stringify(subdoc)}`); } return false; } else { return true; } }); } // Validates a single Text or Number function validateNumberOrText(fieldName, doc) { if (typeof doc == 'string') { return true; } else if (typeof doc == 'number') { return true; } else { setError(`"${fieldName}" must be text or a number, not ${JSON.stringify(doc)}`); return false; } } // Validates a single Boolean function validateBoolean(fieldName, doc) { if (typeof doc != 'boolean') { setError(`"${fieldName}" must be a boolean (ie. "true" or "false"), not ${JSON.stringify(subdoc)}`); return false; } else { return true; } } // Validates a single Date function validateDate(fieldName, doc) { let re = /^\d{4}-\d{2}-\d{2}$/; if (typeof doc != 'string') { setError(`"${fieldName}" must be a date, not ${JSON.stringify(doc)}`); return false; } else if (!doc.match(re)) { setError(`"${fieldName}" must be a date in the format YYYY-MM-DD, not ${JSON.stringify(doc)}`); return false; } else { return true; } } // Validates a CreativeWork or an array of CreativeWork function validateCreativeWorks(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validateCreativeWork(fieldName, subdoc); }); } // Validates a single CreativeWork function validateCreativeWork(fieldName, doc) { if (!Array.isArray(doc) && typeof doc == 'object') { var id = doc["id"] || doc["@id"]; if (id !== undefined && !isUrl(id)) { setError(`"${fieldName}" has an invalid URI as id: ${JSON.stringify(id)}"`); return false; } var type = doc["type"] || doc["@type"]; if (type === undefined) { if (id === undefined) { setError(`"${fieldName}" must be a (list of) CreativeWork object, but it is missing a type/@type.`); return false; } else { // FIXME: we have an @id but no @type, what should we do? return true; } } else if (type != "CreativeWork" || type != "schema:CreativeWork" || type != "http://schema.org/CreativeWork") { // FIXME: is the first variant allowed? setError(`"${fieldName}" must be a (list of) CreativeWork object, not ${JSON.stringify(doc)}`); return false; } else { return true; } // TODO: check other fields } else if (typeof doc == 'string') { if (!isUrl(doc)) { setError(`"${fieldName}" must be an URI or CreativeWork object, not: ${JSON.stringify(id)}"`); return false; } else { return true; } } else { setError(`"${fieldName}" must be a CreativeWork object or URI, not ${JSON.stringify(doc)}`); return false; } } // Validates a Person, Organization or an array of these function validateActors(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validateActor(fieldName, subdoc); }); } // Validates a Person or an array of Person function validatePersons(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validatePerson(fieldName, subdoc); }); } // Validates an Organization or an array of Organization function validateOrganizations(fieldName, doc) { return validateListOrSingle(fieldName, doc, (subdoc, inList) => { return validateOrganization(fieldName, doc); }); } // Validates a single Person or Organization function validateActor(fieldName, doc) { if (!Array.isArray(doc) && typeof doc == 'object') { var id = doc["id"] || doc["@id"]; if (id !== undefined && !isUrl(id)) { setError(`"${fieldName}" has an invalid URI as id: ${JSON.stringify(id)}"`); return false; } var type = getDocumentType(doc); if (type === undefined) { if (id === undefined) { - setError(`"${fieldName}" must be a (list of) Person or Organization object(s), but it is missing a type/@type.`); + setError(`"${fieldName}" must be a (list of) Person or Organization object(s) or an URI, but is missing a type/@type.`); return false; } else { // FIXME: we have an @id but no @type, what should we do? return true; } } else if (isCompactTypeEqual(type, "Person")) { return validatePerson(fieldName, doc); } else if (isCompactTypeEqual(type, "Organization")) { return validateOrganization(fieldName, doc); } else { - setError(`"${fieldName}" type must be a (list of) Person or Organization object(s), not ${type}`); + setError(`"${fieldName}" type must be "Person" or "Organization", not ${JSON.stringify(type)}`); return false; } } else if (typeof doc == 'string') { if (!isUrl(doc)) { setError(`"${fieldName}" must be an URI or a Person or Organization object, not: ${JSON.stringify(id)}"`); return false; } else { return true; } } else { - setError(`"${fieldName}" must be a Person or Organization object or an URI, not ${JSON.stringify(doc)}`); + setError(`"${fieldName}" must be a Person or Organization object or an URI, not ${JSON.stringify(doc)}.`); return false; } } // Validates a single Person object (assumes type/@type was already validated) -function validatePerson(fieldName, doc) { - // TODO - return true; +function validatePerson(parentFieldName, doc) { + // TODO: validate id/@id + if (!isCompactTypeEqual(getDocumentType(doc), "Person")) { + setError(`"${fieldName}" type must be a (list of) Person object(s), not ${JSON.stringify(type)}`); + return false; + } + else { + return Object.entries(doc).every((entry) => { + var fieldName = entry[0]; + var subdoc = entry[1]; + if (fieldName == "type" || fieldName == "@type") { + // Was checked before + return true; + } + else { + var validator = personFieldValidators[fieldName]; + if (validator === undefined) { + // TODO: find if it's a field that belongs to another type, + // and suggest that to the user + setError(`Unknown field "${fieldName}" in "${parentFieldName}".`) + return false; + } + else { + return validator(fieldName, subdoc); + } + } + }); + } } // Validates a single Organization object (assumes type/@type was already validated) function validateOrganization(fieldName, doc) { - // TODO + // TODO: validate id/@id + if (!isCompactTypeEqual(getDocumentType(doc), "Organization")) { + setError(`"${fieldName}" type must be a (list of) Organization object(s), not ${type}`); + return false; + } return true; } var softwareFieldValidators = { + "@id": validateUrl, + "id": validateUrl, + "codeRepository": validateUrls, "programmingLanguage": noValidation, "runtimePlatform": validateTexts, "targetProduct": noValidation, // TODO: validate SoftwareApplication "applicationCategory": validateTextsOrUrls, "applicationSubCategory": validateTextsOrUrls, "downloadUrl": validateUrls, "fileSize": validateText, // TODO "installUrl": validateUrls, "memoryRequirements": validateTextsOrUrls, "operatingSystem": validateTexts, "permissions": validateTexts, "processorRequirements": validateTexts, "releaseNotes": validateTextsOrUrls, "softwareHelp": validateCreativeWorks, "softwareRequirements": noValidation, // TODO: validate SoftwareSourceCode "softwareVersion": validateText, // TODO? "storageRequirements": validateTextsOrUrls, "supportingData": noValidation, // TODO "author": validateActors, "citation": validateCreativeWorks, // TODO "contributor": validateActors, "copyrightHolder": validateActors, "copyrightYear": validateNumbers, "creator": validateActors, "dateCreated": validateDate, "dateModified": validateDate, "datePublished": validateDate, "editor": validatePersons, "encoding": noValidation, "fileFormat": validateTextsOrUrls, "funder": validateActors, "keywords": validateTexts, "license": validateCreativeWorks, // TODO "producer": validateActors, "provider": validateActors, "publisher": validateActors, "sponsor": validateActors, "version": validateNumberOrText, "isAccessibleForFree": validateBoolean, "isPartOf": validateCreativeWorks, "hasPart": validateCreativeWorks, "position": noValidation, "identifier": noValidation, // TODO "description": validateText, "name": validateText, "sameAs": validateUrls, "url": validateUrls, "relatedLink": validateUrls, "softwareSuggestions": noValidation, // TODO: validate SoftwareSourceCode "maintainer": validateActors, "contIntegration": validateUrls, "buildInstructions": validateUrls, "developmentStatus": validateText, // TODO: use only repostatus strings? "embargoDate": validateDate, "funding": validateText, "issueTracker": validateUrls, "referencePublication": noValidation, // TODO? "readme": validateUrls, }; var personFieldValidators = { + "@id": validateUrl, + "id": validateUrl, + "givenName": validateText, "familyName": validateText, "email": validateText, "affiliation": validateOrganizations, "identifier": validateUrls, - "name": validateText, + "name": validateText, // TODO: this is technically valid, but should be allowed here? }; function validateDocument(doc) { if (!Array.isArray(doc) && typeof doc != 'object') { setError("Document must be an object (starting and ending with { and }), not ${typeof doc}.") return false; } // TODO: validate id/@id // TODO: check there is either type or @type but not both var type = getDocumentType(doc); if (type === undefined) { setError("Missing type (must be SoftwareSourceCode or SoftwareApplication).") return false; } else if (!isCompactTypeEqual(type, "SoftwareSourceCode") && !isCompactTypeEqual(type, "SoftwareApplication")) { // Check this before other fields, as a wrong type error is more // understandable than "invalid field". - setError(`Wrong document type: must be SoftwareSourceCode or SoftwareApplication, not ${JSON.stringify(type)}`) + setError(`Wrong document type: must be "SoftwareSourceCode" or "SoftwareApplication", not ${JSON.stringify(type)}`) return false; } else { return Object.entries(doc).every((entry) => { var fieldName = entry[0]; var subdoc = entry[1]; if (fieldName == "@context") { if (subdoc == "https://doi.org/10.5063/schema/codemeta-2.0") { return true; } else { setError(`@context must be "https://doi.org/10.5063/schema/codemeta-2.0", not ${JSON.stringify(subdoc)}`); return false; } } else if (fieldName == "type" || fieldName == "@type") { // Was checked before return true; } else { var validator = softwareFieldValidators[fieldName]; if (validator === undefined) { // TODO: find if it's a field that belongs to another type, // and suggest that to the user setError(`Unknown field "${fieldName}".`) return false; } else { return validator(fieldName, subdoc); } } }); } } function parseAndValidateCodemeta(showPopup) { var codemetaText = document.querySelector('#codemetaText').innerText; var doc; try { doc = JSON.parse(codemetaText); } catch (e) { setError(`Could not read codemeta document because it is not valid JSON (${e}). Check for missing or extra quote, colon, or bracket characters.`); return; } setError(""); var isValid = validateDocument(doc); if (showPopup) { if (isValid) { alert('Document is valid!') } else { alert('Document is invalid.'); } } return doc; }