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 @@
CodeMeta generator
+
Most fields are optional. Mandatory fields will be highlighted when generating Codemeta.
codemeta.json:
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);
+}