Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WIP] Implement links validation #9

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions addon/-private/errors/links-error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export const LINKS_ERROR_TYPES = {
INVALID_MEMBER: uniqueErrorId(),
VALUE_MUST_BE_OBJECT: uniqueErrorId(),
OBJECT_MUST_NOT_BE_EMPTY: uniqueErrorId(),
INVALID_SELF: uniqueErrorId(),
INVALID_RELATED: uniqueErrorId(),
INVALID_PAGINATION: uniqueErrorId(),
};

export interface ILinksErrorOptions {
Expand Down Expand Up @@ -54,7 +57,17 @@ function buildMetaErrorMessage(options: ILinksErrorOptions) {

case LINKS_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY:
return `'${path}.${member}' MUST have at least one member: found an empty object.`;

case LINKS_ERROR_TYPES.INVALID_SELF:
return `'${path}.${member}' MUST contain self as string URLs or an object`;

case LINKS_ERROR_TYPES.INVALID_RELATED:
return `'${path}.${member}' MUST contain related as string URLs or an object`;

case LINKS_ERROR_TYPES.INVALID_PAGINATION:
return `'${path}.${member}' included pagination MUST be null, string URL or an object`;
}


return 'DocumentError';
}
84 changes: 63 additions & 21 deletions addon/-private/validate-links.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { LinksError, LINKS_ERROR_TYPES} from './errors/links-error';
import memberPresent from './utils/member-present';
import isPlainObject from './utils/is-plain-object';

/**
* `links` MUST be an object if present
* each member MUST be a string URL or a link-object
Expand Down Expand Up @@ -30,36 +29,79 @@ import isPlainObject from './utils/is-plain-object';
* @param path
* @returns {boolean}
*/

export default function validateLinks({ document, validator, target, issues, path }) {
let { errors } = issues;
function triggerError(code) {
errors.push(new LinksError({
code,
value: target.links,
member: 'links',
target,
validator,
document,
path
}));
}

if (memberPresent(target, 'links')) {
if (!isPlainObject(target.links)) {
errors.push(new LinksError({
code: LINKS_ERROR_TYPES.VALUE_MUST_BE_OBJECT,
value: target.links,
member: 'links',
target,
validator,
document,
path
}));
triggerError(LINKS_ERROR_TYPES.VALUE_MUST_BE_OBJECT);

return false;
} else if (Object.keys(target.links).length === 0) {
errors.push(
new LinksError({
code: LINKS_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY,
value: target.links,
member: 'links',
target,
validator,
document,
path
})
);
triggerError(LINKS_ERROR_TYPES.OBJECT_MUST_NOT_BE_EMPTY);

return false;
} else if (target.links.self) {
const self = target.links.self;

if (!isPlainObject(self) && typeof self !== 'string') {
triggerError(LINKS_ERROR_TYPES.INVALID_SELF);

return false;
}

if (self.href && typeof self.href !== 'string') {
triggerError(LINKS_ERROR_TYPES.INVALID_SELF);

return false;
}
} else if (target.links.related) {
const related = target.links.related;

if (!isPlainObject(related) && typeof related !== 'string') {
triggerError(LINKS_ERROR_TYPES.INVALID_RELATED);

return false;
}
} else if (target.links.first || target.links.next || target.links.prev || target.links.last) {
const pagination = [
target.links.first,
target.links.next,
target.links.prev,
target.links.last,
];

pagination.forEach((link) => {
if (link) {
if (typeof link !== 'string' && !isPlainObject(link)) {
triggerError(LINKS_ERROR_TYPES.INVALID_PAGINATION);

return false;
}

return true;
} else {
if (link === null) {
return true;
} else {
triggerError(LINKS_ERROR_TYPES.INVALID_PAGINATION);

return false;
}
}
});
}
}
return true;
Expand Down
179 changes: 171 additions & 8 deletions tests/unit/invalid-document-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,31 +606,194 @@ module('Unit | Document', function(hooks: ModuleContext) {
);
});

todo(
test(
'links MAY contain `self`, `related` and the pagination links `first`, `last`, `prev` and `next`',
function(assert) {
assert.notOk('Not Implemented');
let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } };
let linksSelf = buildDoc(fakeDoc, { links: { self: 'https://api.example.com/foos/1' } });
let linksRelated = buildDoc(fakeDoc, { links: { related: 'https://api.example.com/foos/1/bars' } });

assert.doesNotThrowWith(
() => {
push(linksSelf);
},
`'<document>.links' MAY contain self`,
'we do not throw for self in links'
);

assert.doesNotThrowWith(
() => {
push(linksRelated);
},
`'<document>.links' MAY contain related`,
'we do not throw for related in links'
);
}
);

todo(
test(
'included `self` and `related` links MUST either be string URLs or an object with members `href` (a string URL) and an optional `meta` object',
function(assert) {
assert.notOk('Not Implemented');
let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } };
let linksSelf1 = buildDoc(fakeDoc, { links: { self: ['https://api.example.com/foos/1/bars'] } });
let linksSelf2 = buildDoc(fakeDoc, { links: { self: 'https://api.example.com/foos/1/bars' } });
let linksSelf3 = buildDoc(fakeDoc, { links: { self: { href: 'https://api.example.com/foos/1/bars' } } });
let linksSelf4 = buildDoc(fakeDoc, { links: { self: { href: ['https://api.example.com/foos/1/bars'] } } });
let linksSelf5 = buildDoc(fakeDoc, { links: { self: { href: 'https://api.example.com/foos/1/bars', meta: { count: 10 } } } });

let linksRelated1 = buildDoc(fakeDoc, { links: { related: 'https://api.example.com/foos/1/bars' } });
let linksRelated2 = buildDoc(fakeDoc, { links: { related: ['https://api.example.com/foos/1/bars'] } });
let linksRelated3 = buildDoc(fakeDoc, { links: { related: { href: 'https://api.example.com/foos/1/bars' } } });
let linksRelated4 = buildDoc(fakeDoc, { links: { related: { href: 'https://api.example.com/foos/1/bars', meta: { count: 10 } } } });

assert.throwsWith(
() => {
push(linksSelf1);
},
`'<document>.links' MUST contain self as string URLs or an object`,
'we throw for self as array'
);

assert.doesNotThrowWith(
() => {
push(linksSelf2);
},
`'<document>.links' MUST contain self as string URLs or an object`,
'we do not throw for self as string URL'
);

assert.doesNotThrowWith(
() => {
push(linksSelf3);
},
`'<document>.links' MUST contain self as string URLs or an object`,
'we do not throw for self as object with href but not meta'
);

assert.throwsWith(
() => {
push(linksSelf4);
},
`'<document>.links' MUST contain self as string URLs or an object`,
'we do throw for self as object with href as array'
);

assert.doesNotThrowWith(
() => {
push(linksSelf5);
},
`'<document>.links' MUST contain self as string URLs or an object`,
'we do not throw for self as object with href AND with meta object'
);

assert.doesNotThrowWith(
() => {
push(linksRelated1);
},
`'<document>.links' MUST contain related as string URLs or an object`,
'we do not throw for related as string URL'
);

assert.throwsWith(
() => {
push(linksRelated2);
},
`'<document>.links' MUST contain related as string URLs or an object`,
'we do throw for related as array'
);

assert.doesNotThrowWith(
() => {
push(linksRelated3);
},
`'<document>.links' MUST contain related as string URLs or an object`,
'we do not throw for related as object but no meta'
);

assert.doesNotThrowWith(
() => {
push(linksRelated4);
},
`'<document>.links' MUST contain related as string URLs or an object`,
'we do not throw for related as object AND with meta object'
);
}
);

todo(
test(
'included pagination links MUST either be null, string URLs or an object with members `href` (a string URL) and an optional `meta` object',
function(assert) {
assert.notOk('Not Implemented');
let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } };
let linksPagination1 = buildDoc(fakeDoc, { links: { first: null, prev: null, next: null, last: null} });
let linksPagination2 = buildDoc(fakeDoc, { links: { first: 'http://example.com/articles?page[number]=1&page[size]=1' , prev: null, next: 'http://example.com/articles?page[number]=4&page[size]=1', last: null} });
let linksPagination3 = buildDoc(fakeDoc, { links: { first: 'http://example.com/articles?page[number]=1&page[size]=1' , prev: null, next: ['http://example.com/articles?page[number]=4&page[size]=1'], last: null} });
let linksPagination4 = buildDoc(fakeDoc, { links: { first: 'http://example.com/articles?page[number]=1&page[size]=1' , prev: 'http://example.com/articles?page[number]=2&page[size]=1', last: 'http://example.com/articles?page[number]=13&page[size]=1'} });
let linksPagination5 = buildDoc(fakeDoc, { links: { first: 'http://example.com/articles?page[number]=1&page[size]=1' , prev: 'http://example.com/articles?page[number]=2&page[size]=1', next: 'http://example.com/articles?page[number]=4&page[size]=1', last: 'http://example.com/articles?page[number]=13&page[size]=1'} });

assert.doesNotThrowWith(
() => {
push(linksPagination1);
},
`'<document>.links' included pagination MUST be null, string URL or an object`,
'we do not throw for pagination links as null'
);

assert.doesNotThrowWith(
() => {
push(linksPagination2);
},
`'<document>.links' included pagination MUST be null, string URL or an object`,
'we do not throw for some pagination links as null'
);

assert.throwsWith(
() => {
push(linksPagination3);
},
`'<document>.links' included pagination MUST be null, string URL or an object`,
'we do throw for some pagination links as array'
);

assert.throwsWith(
() => {
push(linksPagination4);
},
`'<document>.links' included pagination MUST be null, string URL or an object`,
'we do throw for missing pagination links'
);

assert.doesNotThrowWith(
() => {
push(linksPagination5);
},
`'<document>.links' included pagination MUST be null, string URL or an object`,
'we do not throw for pagination links with string URLs'
);
}
);

todo('(strict-mode) links MAY NOT contain any non-spec members', function(
test('(strict-mode) links MAY NOT contain any non-spec members', function(
assert
) {
assert.notOk('Not Implemented');
let fakeDoc = { data: { type: 'animal', id: '1', attributes: {} } };
let links1 = buildDoc(fakeDoc, { links: { first: null, prev: null, next: null, last: null, custom: 'http://example.com/articles?page[number]=1&page[size]=1'} });
let links2 = buildDoc(fakeDoc, { links: { self_admin: 'http://example.com/articles?page[number]=1&page[size]=1', self: 'http://example.com/articles?page[number]=1&page[size]=1'} });

assert.throwsWith(
() => {
push(links1);
},
`'<document>.links' MAY NOT contain any non-spec members`,
'we do throw for non-spec member'
);

assert.throwsWith(
() => {
push(links2);
},
`'<document>.links' MAY NOT contain any non-spec members`,
'we do throw for non-spec member'
);
});

todo('a document MUST ', function(assert) {
Expand Down
Loading