diff --git a/cypress/integration/validation.js b/cypress/integration/validation.js index 2161406..bd2073c 100644 --- a/cypress/integration/validation.js +++ b/cypress/integration/validation.js @@ -1,1026 +1,1026 @@ /** * Copyright (C) 2020-2021 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"/"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('Things or URLs validation', function() { it('accepts valid Thing', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": { "@type": "SoftwareApplication", "name": "Example Soft", } })) ); 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", "isPartOf": "http://example.org/", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts empty list of Things', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": [], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('accepts list of Things', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": [ { "@type": "SoftwareApplication", "name": "Example Soft", }, { "@type": "SoftwareApplication", "name": "Test Soft", } ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on non-URL string', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "license": "Copyright 2021 Myself", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"license" must be an URL or a CreativeWork/SoftwareSourceCode/SoftwareApplication object, not: "Copyright 2021 Myself"'); }); it('errors on wrong type', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": 42, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a CreativeWork/SoftwareSourceCode/SoftwareApplication object or URI, not 42'); }); it('errors on non-Thing object', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "isPartOf": {}, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with an invalid Thing 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", "isPartOf": [ {}, { "@type": "SoftwareApplication", "name": "Example Soft", } ], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with an invalid Thing 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", "isPartOf": [ { "@type": "SoftwareApplication", "name": "Example Soft", }, {} ], })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); }); 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 URI', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": "http://example.org/~jdoe", })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); 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", "url": "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) Role/Person/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 a (list of) Role/Person/Organization object(s), 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".'); + cy.get('#errorMessage').should('have.text', 'Unknown field "foo".'); }); 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 URI in list', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ "http://example.org/~jadoe", { "@type": "Person", "@id": "http://example.org/~jodoe", "givenName": "John", "familyName": "Doe", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); 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('accepts Person with multiple affiliations', 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", "affiliation": [ { "@type": "Organization", "@id": "http://example.org/", }, { "@type": "Organization", "@id": "http://example.com/", }, ] } })) ); 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) Role/Person/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) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); }); describe('Organization validation', function() { it('accepts valid complete Organization', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": { "@type": "Organization", "@id": "http://example.org/", "url": "https://example.org/", "name": "Example Org", "identifier": "http://example.org/", "address": "Nowhere", } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on Organization 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) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); it('errors on Organization 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 a (list of) Role/Person/Organization object(s), not "SoftwareSourceCode"'); }); it('errors on Organization 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": "Organization", "foo": "bar", }, })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "author".'); + cy.get('#errorMessage').should('have.text', 'Unknown field "foo".'); }); it('errors on Organization 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": "Organization", "email": 32, }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"email" must be text, not 32'); }); it('accepts list of valid Organization', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "author": [ { "@type": "Organization", "@id": "http://example.org/", "name": "Example Org", }, { "@type": "Organization", "@id": "http://example.org/~jodoe", "name": "Example Org", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on list with invalid Organization 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": [ { "name": "Example Org", }, { "@type": "Organization", "name": "Example Org", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); it('errors on list with invalid Organization 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": "Organization", "name": "Example Org", }, { "name": "Example Org", }, ] })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"author" must be a (list of) Role/Person/Organization object(s) or an URI, but is missing a type/@type.'); }); }); describe('CreativeWork validation', function() { it('accepts valid CreativeWork', function() { cy.get('#codemetaText').then((elem) => elem.text(JSON.stringify({ "@context": "https://doi.org/10.5063/schema/codemeta-2.0", "@type": "SoftwareSourceCode", "name": "Small Software", "isPartOf": { "type": "CreativeWork", "name": "Big Creative Work", "author": "http://example.org/~jdoe", "keywords": ["foo", "bar"], } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', ''); }); it('errors on CreativeWork 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", "isPartOf": { } })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s) or an URI, but is missing a type/@type.'); }); it('errors on CreativeWork 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", "isPartOf": { "type": "Person", }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"isPartOf" type must be a (list of) CreativeWork/SoftwareSourceCode/SoftwareApplication object(s), not "Person"'); }); it('errors on CreativeWork 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", "isPartOf": { "type": "CreativeWork", "foo": "bar", }, })) ); cy.get('#validateCodemeta').click(); - cy.get('#errorMessage').should('have.text', 'Unknown field "foo" in "isPartOf".'); + cy.get('#errorMessage').should('have.text', 'Unknown field "foo".'); }); it('errors on CreativeWork 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", "isPartOf": { "type": "CreativeWork", "url": 32, }, })) ); cy.get('#validateCodemeta').click(); cy.get('#errorMessage').should('have.text', '"url" must be an URL (or a list of URLs), not: 32'); }); }); diff --git a/index.html b/index.html index cda6dc8..9ea85b4 100644 --- a/index.html +++ b/index.html @@ -1,394 +1,395 @@ <!doctype html> <!-- Copyright (C) 2019-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 --> <html lang="en"> <head> <meta charset="utf-8"> <title>CodeMeta generator</title> <script src="./js/utils.js"></script> <script src="./js/fields_data.js"></script> <script src="./js/dynamic_form.js"></script> <script src="./js/codemeta_generation.js"></script> + <script src="./js/validation/utils.js"></script> <script src="./js/validation/primitives.js"></script> <script src="./js/validation/things.js"></script> <script src="./js/validation/index.js"></script> <link rel="stylesheet" type="text/css" href="./main.css"> <link rel="stylesheet" type="text/css" href="./codemeta.css"> </head> <body> <header> <h1>CodeMeta generator v3.0</h1> </header> <main> <p>Most fields are optional. Mandatory fields will be highlighted when generating Codemeta.</p> <noscript> <p id="noscriptError"> This application requires Javascript to show dynamic fields in the form, and generate a JSON file; but your browser does not support Javascript. If you cannot use a browser with Javascript support, you can try <a href="https://codemeta.github.io/tools/">one of the other available tools</a> or write the codemeta.json file directly. </p> </noscript> <form id="inputForm"> <fieldset id="fieldsetSoftwareItself" class="leafFieldset"> <legend>The software itself</legend> <p title="The name of the software"> <label for="name">Name</label> <input type="text" name="name" id="name" aria-describedby="name_descr" placeholder="My Software" required="required" /> <span class="field-description" id="name_descr">the software title</span> </p> <p title="a brief description of the software"> <label for="description">Description</label> <textarea rows="4" cols="50" name="description" id="description" placeholder="My Software computes ephemerides and orbit propagation. It has been developed from early ´80." ></textarea> </p> <p title="The date on which the software was created."> <label for="dateCreated">Creation date</label> <input type="text" name="dateCreated" id="dateCreated" placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}" /> </p> <p title="Date of first publication."> <label for="datePublished">First release date</label> <input type="text" name="datePublished" id="datePublished" placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}" /> </p> <p> <label for="license">License(s)</label> <input list="licenses" name="license" id="license" aria-describedby="licenses_descr"> <!-- TODO: insert placeholder --> <datalist id="licenses"> </datalist> <!-- This datalist is be filled automatically --> <br /> <span class="field-description" id="licenses_descr">from <a href="https://spdx.org/license-list">SPDX licence list</a></span> <div id="selected-licenses"> <!-- This div is to be filled as the user selects licenses --> </div> </p> </fieldset> <fieldset id="fieldsetDiscoverabilityAndCitation" class="leafFieldset"> <legend>Discoverability and citation</legend> <p title="Unique identifier"> <label for="identifier">Unique identifier</label> <input type="text" name="identifier" id="identifier" placeholder="10.151.xxxxx" aria-describedby="identifier_descr" /> <br /> <span class="field-description" id="identifier_descr"> such as ISBNs, GTIN codes, UUIDs etc.. <a href="http://schema.org/identifier">http://schema.org/identifier</a> </span> </p> <!-- TODO:define better I looked at the schema.org definition of identifier (https://schema.org/identifier), it can be text, url or PropertyValue. Used as follows in data representation with microdata: <div property="identifier" typeof="PropertyValue"> <span property="propertyID">DOI</span>: <span property="value">10.151.xxxxx</span> </div> we can use that with identifier-type and identifier-value to have a clearer idea of what needs to be in the input. --> <p title="Type of the software application"> <label for="applicationCategory">Application category</label> <input type="text" name="applicationCategory" id="applicationCategory" placeholder="Astronomy" /> </p> <p title="Comma-separated list of keywords"> <label for="keywords">Keywords</label> <input type="text" name="keywords" id="keywords" placeholder="ephemerides, orbit, astronomy" /> </p> <p title="Funding / grant"> <label for="funding">Funding</label> <input type="text" name="funding" id="funding" aria-describedby="funding_descr" placeholder="PRA_2018_73"/> <br /> <span class="field-description" id="funding_descr">grant funding software development</span> </p> <p title="Funding / organization"> <label for="funder">Funder</label> <input type="text" name="funder" id="funder" aria-describedby="funder_descr" placeholder="Università di Pisa"/> <br /> <span class="field-description" id="funder_descr">organization funding software development</span> </p> Authors and contributors can be added below </fieldset> <fieldset id="fieldsetDevelopmentCommunity" class="leafFieldset"> <legend>Development community / tools</legend> <p title="Link to the repository where the un-compiled, human readable code and related code is located (SVN, Git, GitHub, CodePlex, institutional GitLab instance, etc.)."> <label for="codeRepository">Code repository</label> <input type="URL" name="codeRepository" id="codeRepository" placeholder="git+https://github.com/You/RepoName.git" /> </p> <p title="Link to continuous integration service (Travis-CI, Gitlab CI, etc.)."> <label for="contIntegration">Continuous integration</label> <input type="URL" name="contIntegration" id="contIntegration" placeholder="https://travis-ci.org/You/RepoName" /> </p> <p title="Link to a place for users/developpers to report and manage bugs (JIRA, GitHub issues, etc.)."> <label for="issueTracker">Issue tracker</label> <input type="URL" name="issueTracker" id="issueTracker" placeholder="https://github.com/You/RepoName/issues" /> </p> <p title="Related document, software, tools"> <label for="relatedLink">Related links</label> <br /> <textarea rows="4" cols="50" name="relatedLink" id="relatedLink"></textarea> </fieldset> <fieldset id="fieldsetRuntime" class="leafFieldset"> <legend>Run-time environment</legend> <p title="Programming Languages, separated by commas"> <label for="programmingLanguage">Programming Language</label> <input type="text" name="programmingLanguage" id="programmingLanguage" placeholder="C#, Java, Python 3" /> </p> <p title="Runtime Platforms, separated by commas"> <label for="runtimePlatform">Runtime Platform</label> <input type="text" name="runtimePlatform" id="runtimePlatform" placeholder=".NET, JVM" /> </p> <p title="Operating Systems, separated by commas"> <label for="operatingSystem">Operating System</label> <input type="text" name="operatingSystem" id="operatingSystem" placeholder="Android 1.6, Linux, Windows, macOS" /> </p> <p title="Required software to run/use this one."> <label for="softwareRequirements">Other software requirements</label> <br /> <textarea rows="4" cols="50" name="softwareRequirements" id="softwareRequirements" placeholder= "Python 3.4 https://github.com/psf/requests"></textarea> </fieldset> <fieldset id="fieldsetCurrentVersion" class="leafFieldset"> <legend>Current version of the software</legend> <p title="Version number of the software"> <label for="version">Version number</label> <input type="text" name="version" id="version" placeholder="1.0.0" /> </p> <p title="The date on which the software was most recently modified."> <label for="dateModified">Release date</label> <input type="text" name="dateModified" id="dateModified" placeholder="YYYY-MM-DD" pattern="\d{4}-\d{2}-\d{2}" /> </p> <p title="Download link"> <label for="downloadUrl">Download URL</label> <input type="URL" name="downloadUrl" id="downloadUrl" placeholder="https://example.org/MySoftware.tar.gz" /> </p> <p title="a brief description of the software"> <label for="releaseNotes">Release notes</label> <br /> <textarea rows="4" cols="50" name="releaseNotes" id="releaseNotes" placeholder= "Change log: this and that; Bugfixes: that and this." ></textarea> </p> <!--TODO: referencePublication as ScholarlyArticle array --> </fieldset> <fieldset id="review_container" class="leafFieldset"> <legend>Editorial review</legend> <p title="Scholarly article describing this software"> <label for="referencePublication">Reference Publication</label> <input type="URL" name="referencePublication" id="referencePublication" placeholder="https://doi.org/10.1000/xyz123" /> </p> <p title="Part or facet of the object being review "> <label for="reviewAspect">Review aspect</label> <input type="text" name="reviewAspect" id="reviewAspect" placeholder="Object facet" /> </p> <p title="The actual body of the review "> <label for="reviewBody">Review body</label> <textarea rows="4" cols="50" name="reviewBody" id="reviewBody" placeholder="Review about my software." ></textarea> </p> </fieldset> <fieldset id="fieldsetAdditionalInfo" class="leafFieldset"> <legend>Additional Info</legend> <p title="Development Status"> <label for="developmentStatus">Development Status</label> <datalist id="developmentStatuses"> <option value="concept"> <option value="wip"> <option value="suspended"> <option value="abandoned"> <option value="active"> <option value="inactive"> <option value="unsupported"> <option value="moved"> </datalist> <input list="developmentStatuses" id="developmentStatus" aria-describedby="developmentStatuses_descr" pattern="concept|wip|suspended|abandoned|active|inactive|unsupported|moved"> <br /> <span class="field-description" id="developmentStatuses_descr"> see <a href="http://www.repostatus.org">www.repostatus.org</a> for details </span> </p> <p title="Source Code of"> <label for="isSourceCodeOf">Is Source Code of</label> <input type="text" name="isSourceCodeOf" id="isSourceCodeOf" placeholder="Bigger Application" /> </p> <p title="Part of"> <label for="isPartOf">Is part of</label> <input type="URL" name="isPartOf" id="isPartOf" placeholder="http://The.Bigger.Framework.org" /> </p> </fieldset> <div class="dynamicFields"> <fieldset class="persons" id="author_container"> <legend>Authors</legend> <input type="hidden" id="author_nb" value="0" /> <div id="addRemoveAuthor"> <input type="button" id="author_add" value="Add one" onclick="addPerson('author', 'Author');" /> <input type="button" id="author_remove" value="Remove last" onclick="removePerson('author');" /> </div> </fieldset> <fieldset class="persons" id="contributor_container"> <legend>Contributors</legend> <p>Order of contributors does not matter.</p> <input type="hidden" id="contributor_nb" value="0" /> <div id="addRemoveContributor"> <input type="button" id="contributor_add" value="Add one" onclick="addPerson('contributor', 'Contributor');" /> <input type="button" id="contributor_remove" value="Remove last" onclick="removePerson('contributor');" /> </div> </fieldset> </div> </form> <form> <input type="button" id="generateCodemetaV3" value="Generate codemeta.json v3.0" disabled title="Creates a codemeta.json v3.0 file below, from the information provided above." /> <input type="button" id="generateCodemetaV2" value="Generate codemeta.json v2.0" disabled title="Creates a codemeta.json v2.0 file below, from the information provided above." /> <input type="button" id="resetForm" value="Reset form" title="Erases all fields." /> <input type="button" id="validateCodemeta" value="Validate codemeta.json" disabled title="Checks the codemeta.json file below is valid, and displays errors." /> <input type="button" id="importCodemeta" value="Import codemeta.json" disabled title="Fills the fields above based on the codemeta.json file below." /> </form> <p id="errorMessage"> </p> <p>codemeta.json:</p> <pre contentEditable="true" id="codemetaText"></pre> </main> <footer> <p style="text-align:center;"> Do you want to improve this tool ? Check out the <a href="https://github.com/codemeta/codemeta-generator"> CodeMeta-generator repository</a> <br /> Join the <a href="https://github.com/codemeta/codemeta">CodeMeta community</a> discussion <br /> The CodeMeta vocabulary - <a href="https://doi.org/10.5063/schema/codemeta-2.0">v2.0</a> - <a href="https://w3id.org/codemeta/3.0">v3.0</a> </p> <h2 style="text-align:right;">Contributed by</h2> <p style="text-align:right;"> <a href="https://www.softwareheritage.org/save-and-reference-research-software/"> <img alt="Software Heritage" src="https://annex.softwareheritage.org/public/logo/software-heritage-logo-title-motto.svg" width="300"> </a> </p> </footer> <script src="./js/libs/jsonld/jsonld.min.js"></script> <script> Promise.all([loadSpdxData(), loadContextData()]).then(results => { const [licenses, contexts] = results; SPDX_LICENSES = licenses; SPDX_LICENSE_IDS = licenses.map(license => license['licenseId']); initJsonldLoader(contexts); initFields(); initCallbacks(); loadStateFromStorage(); }); </script> </body> </html> diff --git a/js/codemeta_generation.js b/js/codemeta_generation.js index 6dd111d..195e9a0 100644 --- a/js/codemeta_generation.js +++ b/js/codemeta_generation.js @@ -1,437 +1,439 @@ /** * Copyright (C) 2019-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 */ "use strict"; -const LOCAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld"; -const LOCAL_CONTEXT_URL = "local"; +const INTERNAL_CONTEXT_PATH = "./data/contexts/codemeta-local.jsonld"; +const INTERNAL_CONTEXT_URL = "internal"; const CODEMETA_CONTEXTS = { "2.0": { path: "./data/contexts/codemeta-2.0.jsonld", url: "https://doi.org/10.5063/schema/codemeta-2.0" }, "3.0": { path: "./data/contexts/codemeta-3.0.jsonld", url: "https://w3id.org/codemeta/3.0" } } const SPDX_PREFIX = 'https://spdx.org/licenses/'; const loadContextData = async () => { const [contextLocal, contextV2, contextV3] = await Promise.all([ - fetch(LOCAL_CONTEXT_PATH).then(response => response.json()), + fetch(INTERNAL_CONTEXT_PATH).then(response => response.json()), fetch(CODEMETA_CONTEXTS["2.0"].path).then(response => response.json()), fetch(CODEMETA_CONTEXTS["3.0"].path).then(response => response.json()) ]); return { - [LOCAL_CONTEXT_URL]: contextLocal, + [INTERNAL_CONTEXT_URL]: contextLocal, [CODEMETA_CONTEXTS["2.0"].url]: contextV2, [CODEMETA_CONTEXTS["3.0"].url]: contextV3 } } const getJsonldCustomLoader = contexts => { return url => { const xhrDocumentLoader = jsonld.documentLoaders.xhr(); if (url in contexts) { return { contextUrl: null, document: contexts[url], documentUrl: url }; } return xhrDocumentLoader(url); } }; const initJsonldLoader = contexts => { jsonld.documentLoader = getJsonldCustomLoader(contexts); }; function emptyToUndefined(v) { if (v == null || v == "") return undefined; else return v; } function getIfSet(query) { return emptyToUndefined(document.querySelector(query).value); } function setIfDefined(query, value) { if (value !== undefined) { document.querySelector(query).value = value; } } function getLicenses() { let selectedLicenses = Array.from(document.getElementById("selected-licenses").children); return selectedLicenses.map(licenseDiv => SPDX_PREFIX + licenseDiv.children[0].innerText); } // Names of codemeta properties with a matching HTML field name const directCodemetaFields = [ 'codeRepository', 'contIntegration', 'dateCreated', 'datePublished', 'dateModified', 'downloadUrl', 'issueTracker', 'name', 'version', 'identifier', 'description', 'applicationCategory', 'releaseNotes', 'funding', 'developmentStatus', 'isSourceCodeOf', 'isPartOf', 'referencePublication' ]; const splittedCodemetaFields = [ ['keywords', ','], ['programmingLanguage', ','], ['runtimePlatform', ','], ['operatingSystem', ','], ['softwareRequirements', '\n'], ['relatedLink', '\n'], ] // Names of codemeta properties with a matching HTML field name, // in a Person object const directPersonCodemetaFields = [ 'givenName', 'familyName', 'email', 'affiliation', ]; const directRoleCodemetaFields = [ 'roleName', 'startDate', 'endDate', ]; const directReviewCodemetaFields = [ 'reviewAspect', 'reviewBody' ]; const crossCodemetaFields = { "contIntegration": ["contIntegration", "continuousIntegration"], // "embargoDate": ["embargoDate", "embargoEndDate"], Not present in the form yet TODO ? }; function generateShortOrg(fieldName) { var affiliation = getIfSet(fieldName); if (affiliation !== undefined) { if (isUrl(affiliation)) { return { "@type": "Organization", "@id": affiliation, }; } else { return { "@type": "Organization", "name": affiliation, }; } } else { return undefined; } } function generatePerson(idPrefix) { var doc = { "@type": "Person", } var id = getIfSet(`#${idPrefix}_id`); if (id !== undefined) { doc["@id"] = id; } directPersonCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${idPrefix}_${item}`); }); doc["affiliation"] = generateShortOrg(`#${idPrefix}_affiliation`); return doc; } function generateRole(id) { const doc = { "@type": "Role" }; directRoleCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${id} .${item}`); }); return doc; } function generateRoles(idPrefix, person) { const roles = []; const roleNodes = document.querySelectorAll(`ul[id^=${idPrefix}_role_`); roleNodes.forEach(roleNode => { const role = generateRole(roleNode.id); role["schema:author"] = person; // Prefix with "schema:" to prevent it from expanding into a list roles.push(role); }); return roles; } function generatePersons(prefix) { var persons = []; var nbPersons = getNbPersons(prefix); for (let personId = 1; personId <= nbPersons; personId++) { const idPrefix = `${prefix}_${personId}`; const person = generatePerson(idPrefix); persons.push(person); const roles = generateRoles(idPrefix, person); if (roles.length > 0) { persons = persons.concat(roles); } } return persons; } function generateReview() { const doc = { "@type": "Review" }; directReviewCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet(`#${item}`); }); return doc; } async function buildExpandedJson() { var doc = { - "@context": LOCAL_CONTEXT_URL, + "@context": INTERNAL_CONTEXT_URL, "@type": "SoftwareSourceCode", }; let licenses = getLicenses(); if (licenses.length > 0) { doc["license"] = licenses; } // Generate most fields directCodemetaFields.forEach(function (item, index) { doc[item] = getIfSet('#' + item) }); doc["funder"] = generateShortOrg('#funder', doc["affiliation"]); const review = generateReview(); if (review["reviewAspect"] || review["reviewBody"]) { doc["review"] = generateReview(); } // Generate simple fields parsed simply by splitting splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; const value = getIfSet('#' + id); if (value !== undefined) { doc[id] = value.split(separator).map(trimSpaces); } }); // Generate dynamic fields var authors = generatePersons('author'); if (authors.length > 0) { doc["author"] = authors; } var contributors = generatePersons('contributor'); if (contributors.length > 0) { doc["contributor"] = contributors; } for (const [key, items] of Object.entries(crossCodemetaFields)) { items.forEach(item => { doc[item] = doc[key]; }); } return await jsonld.expand(doc); } // v2.0 is still default version for generation, for now async function generateCodemeta(codemetaVersion = "2.0") { var inputForm = document.querySelector('#inputForm'); var codemetaText, errorHTML; + let compacted; if (inputForm.checkValidity()) { const expanded = await buildExpandedJson(); - const compacted = await jsonld.compact(expanded, CODEMETA_CONTEXTS[codemetaVersion].url); + compacted = await jsonld.compact(expanded, CODEMETA_CONTEXTS[codemetaVersion].url); codemetaText = JSON.stringify(compacted, null, 4); errorHTML = ""; } else { codemetaText = ""; errorHTML = "invalid input (see error above)"; inputForm.reportValidity(); } document.querySelector('#codemetaText').innerText = codemetaText; setError(errorHTML); // Run validator on the exported value, for extra validation. // If this finds a validation, it means there is a bug in our code (either // generation or validation), and the generation MUST NOT generate an // invalid codemeta file, regardless of user input. - if (codemetaText && !validateDocument(JSON.parse(codemetaText))) { + const isValid = codemetaText && (await parseAndValidateCodemeta(false)); + if (!isValid) { alert('Bug detected! The data you wrote is correct; but for some reason, it seems we generated an invalid codemeta.json. Please report this bug at https://github.com/codemeta/codemeta-generator/issues/new and copy-paste the generated codemeta.json file. Thanks!'); } if (codemetaText) { // For restoring the form state on page reload sessionStorage.setItem('codemetaText', codemetaText); } } // Imports a single field (name or @id) from an Organization. function importShortOrg(fieldName, doc) { if (doc !== undefined) { // Use @id if set, else use name setIfDefined(fieldName, doc["name"]); setIfDefined(fieldName, getDocumentId(doc)); } } function importReview(doc) { if (doc !== undefined) { directReviewCodemetaFields.forEach(item => { setIfDefined('#' + item, doc[item]); }); } } function authorsEqual(author1, author2) { // TODO should test more properties for equality? return author1.givenName === author2.givenName && author1.familyName === author2.familyName && author1.email === author2.email; } function getSingleAuthorsFromRoles(docs) { return docs.filter(doc => getDocumentType(doc) === "Role") .map(doc => doc["schema:author"]) .reduce((authorSet, currentAuthor) => { const foundAuthor = authorSet.find(author => authorsEqual(author, currentAuthor)); if (!foundAuthor) { return authorSet.concat([currentAuthor]); } else { return authorSet; } }, []); } function importRoles(personPrefix, roles) { roles.forEach(role => { const roleId = addRole(`${personPrefix}`); directRoleCodemetaFields.forEach(item => { setIfDefined(`#${personPrefix}_${item}_${roleId}`, role[item]); }); }); } function importPersons(prefix, legend, docs) { if (docs === undefined) { return; } const authors = docs.filter(doc => getDocumentType(doc) === "Person"); const authorsFromRoles = getSingleAuthorsFromRoles(docs); const allAuthorDocs = authors.concat(authorsFromRoles) .reduce((authors, currentAuthor) => { if (!authors.find(author => authorsEqual(author, currentAuthor))) { authors.push(currentAuthor); } return authors; }, []); allAuthorDocs.forEach(function (doc, index) { var personId = addPerson(prefix, legend); setIfDefined(`#${prefix}_${personId}_id`, getDocumentId(doc)); directPersonCodemetaFields.forEach(function (item, index) { setIfDefined(`#${prefix}_${personId}_${item}`, doc[item]); }); importShortOrg(`#${prefix}_${personId}_affiliation`, doc['affiliation']); const roles = docs.filter(currentDoc => getDocumentType(currentDoc) === "Role") .filter(currentDoc => authorsEqual(currentDoc["schema:author"], doc)); importRoles(`${prefix}_${personId}`, roles); }); } async function importCodemeta() { var inputForm = document.querySelector('#inputForm'); var doc = await parseAndValidateCodemeta(false); resetForm(); if (doc['license'] !== undefined) { if (typeof doc['license'] === 'string') { doc['license'] = [doc['license']]; } doc['license'].forEach(l => { if (l.indexOf(SPDX_PREFIX) !== 0) { return; } let licenseId = l.substring(SPDX_PREFIX.length); insertLicenseElement(licenseId); }); } directCodemetaFields.forEach(function (item, index) { setIfDefined('#' + item, doc[item]); }); importShortOrg('#funder', doc["funder"]); importReview(doc["review"]); // Import simple fields by joining on their separator splittedCodemetaFields.forEach(function (item, index) { const id = item[0]; const separator = item[1]; let value = doc[id]; if (value !== undefined) { if (Array.isArray(value)) { value = value.join(separator); } setIfDefined('#' + id, value); } }); for (const [key, items] of Object.entries(crossCodemetaFields)) { let value = ""; items.forEach(item => { value = doc[item] || value; }); setIfDefined(`#${key}`, value); } importPersons('author', 'Author', doc['author']) importPersons('contributor', 'Contributor', doc['contributor']) } function loadStateFromStorage() { var codemetaText = sessionStorage.getItem('codemetaText') if (codemetaText) { document.querySelector('#codemetaText').innerText = codemetaText; importCodemeta(); } } diff --git a/js/validation/index.js b/js/validation/index.js index 4035f8c..fdfad61 100644 --- a/js/validation/index.js +++ b/js/validation/index.js @@ -1,96 +1,254 @@ /** * 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. */ +const softwareFieldValidators = { + "@id": validateUrl, + "id": validateUrl, -function validateDocument(doc) { - if (!Array.isArray(doc) && typeof doc != 'object') { + "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, // TODO: still in codemeta 2.0, but removed from master + "dateCreated": validateDate, + "dateModified": validateDate, + "datePublished": validateDate, + "editor": validatePersons, + "encoding": noValidation, + "fileFormat": validateTextsOrUrls, + "funder": validateActors, // TODO: may be other types + "keywords": validateTexts, + "license": validateCreativeWorks, + "producer": validateActors, + "provider": validateActors, + "publisher": validateActors, + "sponsor": validateActors, + "version": validateNumberOrText, + "isAccessibleForFree": validateBoolean, + "isSourceCodeOf": validateTextsOrUrls, + "isPartOf": validateCreativeWorks, + "hasPart": validateCreativeWorks, + "position": noValidation, + "identifier": noValidation, // TODO + "description": validateText, + "name": validateText, + "sameAs": validateUrls, + "url": validateUrls, + "relatedLink": validateUrls, + "review": validateReview, + + "softwareSuggestions": noValidation, // TODO: validate SoftwareSourceCode + "maintainer": validateActors, + "contIntegration": validateUrls, + "continuousIntegration": validateUrls, + "buildInstructions": validateUrls, + "developmentStatus": validateText, // TODO: use only repostatus strings? + "embargoDate": validateDate, + "embargoEndDate": validateDate, + "funding": validateText, + "issueTracker": validateUrls, + "referencePublication": noValidation, // TODO? + "readme": validateUrls, +}; + +const creativeWorkFieldValidators = { + "@id": validateUrl, + "id": validateUrl, + + "author": validateActors, + "citation": validateCreativeWorks, // TODO + "contributor": validateActors, + "copyrightHolder": validateActors, + "copyrightYear": validateNumbers, + "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master + "dateCreated": validateDate, + "dateModified": validateDate, + "datePublished": validateDate, + "editor": validatePersons, + "encoding": noValidation, + "funder": validateActors, // TODO: may be other types + "keywords": validateTexts, + "license": validateCreativeWorks, + "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, +}; + +const roleFieldValidators = { + "roleName": validateText, + "startDate": validateDate, + "endDate": validateDate, + + "schema:author": validateActor +}; + +const personFieldValidators = { + "@id": validateUrl, + "id": validateUrl, + + "givenName": validateText, + "familyName": validateText, + "email": validateText, + "affiliation": validateOrganizations, + "identifier": validateUrls, + "name": validateText, // TODO: this is technically valid, but should be allowed here? + "url": validateUrls, +}; + +const organizationFieldValidators = { + "@id": validateUrl, + "id": validateUrl, + + "email": validateText, + "identifier": validateUrls, + "name": validateText, + "address": validateText, + "sponsor": validateActors, + "funder": validateActors, // TODO: may be other types + "isPartOf": validateOrganizations, + "url": validateUrls, + + // TODO: add more? +}; + +const reviewFieldValidators = { + "reviewAspect": validateText, + "reviewBody": validateText, +} + +function switchCodemetaContext(codemetaJSON, contextUrl) { + const previousCodemetaContext = codemetaJSON["@context"]; + codemetaJSON["@context"] = contextUrl; + return previousCodemetaContext; +} + +async function validateTerms(codemetaJSON) { + try { + await jsonld.expand(codemetaJSON, { safe: true }); + } catch (validationError) { + if (validationError.details.event.code === "invalid property") { + setError(`Unknown field "${validationError.details.event.details.property}".`); + return false; + } + } + return true; +} + +function validateCodemetaJSON(codemetaJSON) { + if (!Array.isArray(codemetaJSON) && typeof codemetaJSON != '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); + var type = getDocumentType(codemetaJSON); 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"/"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") { - // Was checked before - return true; - } - else if (fieldName == "type" || fieldName == "@type") { - // Was checked before - return true; - } - else if (isFieldFromOtherVersionToIgnore(fieldName)) { - // Do not check fields from other versions FIXME - 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); - } + return true; +} + +function validateDocument(doc) { + return Object.entries(doc) + .filter(([fieldName]) => !isKeyword(fieldName)) + .every(([fieldName, subdoc]) => { + const compactedFieldName = getCompactType(fieldName); + var validator = softwareFieldValidators[compactedFieldName]; + if (validator === undefined) { + // TODO: find if it's a field that belongs to another type, + // and suggest that to the user + setError(`Unknown field "${compactedFieldName}".`) + return false; + } else { + return validator(compactedFieldName, subdoc); } }); - } } - async function parseAndValidateCodemeta(showPopup) { var codemetaText = document.querySelector('#codemetaText').innerText; - let parsed, doc; + let parsed; try { parsed = 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(parsed); + let isJSONValid = validateCodemetaJSON(parsed); + + const previousCodemetaContext = switchCodemetaContext(parsed, INTERNAL_CONTEXT_URL); + + let areTermsValid = await validateTerms(parsed); + + const expanded = await jsonld.expand(parsed); + const doc = await jsonld.compact(expanded, INTERNAL_CONTEXT_URL); + + switchCodemetaContext(parsed, previousCodemetaContext) + + let isDocumentValid = validateDocument(doc); if (showPopup) { - if (isValid) { + if (isJSONValid && areTermsValid && isDocumentValid) { alert('Document is valid!') } else { alert('Document is invalid.'); } } - parsed["@context"] = LOCAL_CONTEXT_URL; - const expanded = await jsonld.expand(parsed); - doc = await jsonld.compact(expanded, LOCAL_CONTEXT_URL); return doc; } diff --git a/js/validation/primitives.js b/js/validation/primitives.js index 2dc1cd0..6488d14 100644 --- a/js/validation/primitives.js +++ b/js/validation/primitives.js @@ -1,146 +1,150 @@ /** * 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 */ /* * Validators for native schema.org data types. */ +function noValidation(fieldName, doc) { + return true; +} + // 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; } } diff --git a/js/validation/things.js b/js/validation/things.js index c790bb0..542b5bb 100644 --- a/js/validation/things.js +++ b/js/validation/things.js @@ -1,344 +1,158 @@ /** * Copyright (C) 2020-2021 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 */ /* * Validators for codemeta objects derived from http://schema.org/Thing. */ -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 getDocumentId(doc) { - return doc["id"] || doc["@id"]; -} - -function isCompactTypeEqual(type, compactedType) { - // FIXME: are all variants allowed? - return (type == `${compactedType}` - || type == `schema:${compactedType}` - || type == `codemeta:${compactedType}` - || type == `http://schema.org/${compactedType}` - ); -} - -function isFieldFromOtherVersionToIgnore(fieldName) { - return ["codemeta:contIntegration", "codemeta:continuousIntegration", "codemeta:isSourceCodeOf", - "schema:review", "schema:reviewAspect", "schema:reviewBody"].includes(fieldName); -} - -function noValidation(fieldName, doc) { - return true; -} - - // Validates subtypes of Thing, or URIs // // typeFieldValidators is a map: {type => {fieldName => fieldValidator}} function validateThingOrId(parentFieldName, typeFieldValidators, doc) { var acceptedTypesString = Object.keys(typeFieldValidators).join('/'); if (typeof doc == 'string') { if (!isUrl(doc)) { setError(`"${parentFieldName}" must be an URL or a ${acceptedTypesString} object, not: ${JSON.stringify(doc)}`); return false; } else { return true; } } else if (!Array.isArray(doc) && typeof doc == 'object') { return validateThing(parentFieldName, typeFieldValidators, doc); } else { setError(`"${parentFieldName}" must be a ${acceptedTypesString} object or URI, not ${JSON.stringify(doc)}`); return false; } } // Validates subtypes of Thing // // typeFieldValidators is a map: {type => {fieldName => fieldValidator}} function validateThing(parentFieldName, typeFieldValidators, doc) { // TODO: check there is either id or @id but not both // TODO: check there is either type or @type but not both var acceptedTypesString = Object.keys(typeFieldValidators).join('/'); var documentType = getDocumentType(doc); var id = getDocumentId(doc); if (id !== undefined && !isUrl(id)) { setError(`"${fieldName}" has an invalid URI as id: ${JSON.stringify(id)}"`); return false; } if (documentType === undefined) { if (id === undefined) { setError(`"${parentFieldName}" must be a (list of) ${acceptedTypesString} 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; } } for (expectedType in typeFieldValidators) { if (isCompactTypeEqual(documentType, expectedType)) { var fieldValidators = typeFieldValidators[expectedType]; - 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 if (isFieldFromOtherVersionToIgnore(fieldName)) { - // Do not check fields from other versions FIXME - return true; - } - else { - var validator = fieldValidators[fieldName]; + + return Object.entries(doc) + .filter(([fieldName]) => !isKeyword(fieldName)) + .every(([fieldName, subdoc]) => { + const compactedFieldName = getCompactType(fieldName); + var validator = fieldValidators[compactedFieldName]; 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}".`) + setError(`Unknown field "${compactedFieldName}".`) return false; + } else { + return validator(compactedFieldName, subdoc); } - else { - return validator(fieldName, subdoc); - } - } - }); + }); } } setError(`"${parentFieldName}" type must be a (list of) ${acceptedTypesString} object(s), not ${JSON.stringify(documentType)}`); return false; } // 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 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) { return validateThingOrId(fieldName, { "CreativeWork": creativeWorkFieldValidators, "SoftwareSourceCode": softwareFieldValidators, "SoftwareApplication": softwareFieldValidators, }, doc); } // 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, subdoc); }); } // Validates a single Person or Organization function validateActor(fieldName, doc) { return validateThingOrId(fieldName, { "Role": roleFieldValidators, "Person": personFieldValidators, "Organization": organizationFieldValidators, }, doc); } // Validates a single Person object function validatePerson(fieldName, doc) { return validateThingOrId(fieldName, {"Person": personFieldValidators}, doc); } // Validates a single Organization object function validateOrganization(fieldName, doc) { return validateThingOrId(fieldName, {"Organization": organizationFieldValidators}, doc); } function validateReview(fieldName, doc) { return validateThingOrId(fieldName, {"Review": reviewFieldValidators}, doc); } - - -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, // TODO: still in codemeta 2.0, but removed from master - "dateCreated": validateDate, - "dateModified": validateDate, - "datePublished": validateDate, - "editor": validatePersons, - "encoding": noValidation, - "fileFormat": validateTextsOrUrls, - "funder": validateActors, // TODO: may be other types - "keywords": validateTexts, - "license": validateCreativeWorks, - "producer": validateActors, - "provider": validateActors, - "publisher": validateActors, - "sponsor": validateActors, - "version": validateNumberOrText, - "isAccessibleForFree": validateBoolean, - "isSourceCodeOf": validateTextsOrUrls, - "isPartOf": validateCreativeWorks, - "hasPart": validateCreativeWorks, - "position": noValidation, - "identifier": noValidation, // TODO - "description": validateText, - "name": validateText, - "sameAs": validateUrls, - "url": validateUrls, - "relatedLink": validateUrls, - "review": validateReview, - - "softwareSuggestions": noValidation, // TODO: validate SoftwareSourceCode - "maintainer": validateActors, - "contIntegration": validateUrls, - "continuousIntegration": validateUrls, - "buildInstructions": validateUrls, - "developmentStatus": validateText, // TODO: use only repostatus strings? - "embargoDate": validateDate, - "embargoEndDate": validateDate, - "funding": validateText, - "issueTracker": validateUrls, - "referencePublication": noValidation, // TODO? - "readme": validateUrls, -}; - -var creativeWorkFieldValidators = { - "@id": validateUrl, - "id": validateUrl, - - "author": validateActors, - "citation": validateCreativeWorks, // TODO - "contributor": validateActors, - "copyrightHolder": validateActors, - "copyrightYear": validateNumbers, - "creator": validateActors, // TODO: still in codemeta 2.0, but removed from master - "dateCreated": validateDate, - "dateModified": validateDate, - "datePublished": validateDate, - "editor": validatePersons, - "encoding": noValidation, - "funder": validateActors, // TODO: may be other types - "keywords": validateTexts, - "license": validateCreativeWorks, - "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, -}; - -var roleFieldValidators = { - "roleName": validateText, - "startDate": validateDate, - "endDate": validateDate, - - "schema:author": validateActor -}; - -var personFieldValidators = { - "@id": validateUrl, - "id": validateUrl, - - "givenName": validateText, - "familyName": validateText, - "email": validateText, - "affiliation": validateOrganizations, - "identifier": validateUrls, - "name": validateText, // TODO: this is technically valid, but should be allowed here? - "url": validateUrls, -}; - - -var organizationFieldValidators = { - "@id": validateUrl, - "id": validateUrl, - - "email": validateText, - "identifier": validateUrls, - "name": validateText, - "address": validateText, - "sponsor": validateActors, - "funder": validateActors, // TODO: may be other types - "isPartOf": validateOrganizations, - "url": validateUrls, - - // TODO: add more? -}; - -const reviewFieldValidators = { - "reviewAspect": validateText, - "reviewBody": validateText, -} diff --git a/js/validation/utils.js b/js/validation/utils.js new file mode 100644 index 0000000..4f91c86 --- /dev/null +++ b/js/validation/utils.js @@ -0,0 +1,35 @@ +/** + * Copyright (C) 2020-2021 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 + */ + +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 getDocumentId(doc) { + return doc["id"] || doc["@id"]; +} + +function getCompactType(type) { + return type + .replace("schema:", "") + .replace("codemeta:", ""); +} + +function isCompactTypeEqual(type, compactedType) { + // FIXME: are all variants allowed? + return (type == `${compactedType}` + || type == `schema:${compactedType}` + || type == `codemeta:${compactedType}` + || type == `http://schema.org/${compactedType}` + ); +} + +function isKeyword(term) { + return ["@context", "type"].includes(term); +}