Skip to content

Commit ef162fe

Browse files
committed
feat(signposting): add signposting service
1 parent 61cf622 commit ef162fe

3 files changed

Lines changed: 144 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export const LINKSET_TYPE = 'application/linkset';
2+
export const LINKSET_JSON_TYPE = 'application/linkset+json';
3+
4+
export interface SignpostingLink {
5+
rel: string;
6+
href: string;
7+
type: string;
8+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { RendererFactory2, RESPONSE_INIT } from '@angular/core';
2+
import { TestBed } from '@angular/core/testing';
3+
4+
import { SignpostingService } from './signposting.service';
5+
6+
describe('Service: Signposting', () => {
7+
let service: SignpostingService;
8+
let mockResponseInit: ResponseInit;
9+
let createdLinks: Record<string, string>[];
10+
let mockAppendChild: jest.Mock;
11+
12+
beforeEach(() => {
13+
createdLinks = [];
14+
mockAppendChild = jest.fn();
15+
mockResponseInit = { headers: new Headers() };
16+
17+
TestBed.configureTestingModule({
18+
providers: [
19+
SignpostingService,
20+
{ provide: RESPONSE_INIT, useValue: mockResponseInit },
21+
{
22+
provide: RendererFactory2,
23+
useValue: {
24+
createRenderer: () => ({
25+
createElement: jest.fn().mockImplementation(() => {
26+
const link: Record<string, string> = {};
27+
createdLinks.push(link);
28+
return link;
29+
}),
30+
setAttribute: jest.fn().mockImplementation((el, attr, value) => {
31+
el[attr] = value;
32+
}),
33+
appendChild: mockAppendChild,
34+
}),
35+
},
36+
},
37+
],
38+
});
39+
40+
service = TestBed.inject(SignpostingService);
41+
});
42+
43+
it('should set headers using addSignposting', () => {
44+
service.addSignposting('abcde');
45+
const linkHeader = (mockResponseInit.headers as Headers).get('Link');
46+
expect(linkHeader).toBe(
47+
'<https://staging3.osf.io/metadata/abcde/?format=linkset>; rel="linkset"; type="application/linkset", <https://staging3.osf.io/metadata/abcde/?format=linkset%2Bjson>; rel="linkset"; type="application/linkset+json"'
48+
);
49+
});
50+
51+
it('should add link tags using addSignposting', () => {
52+
service.addSignposting('abcde');
53+
54+
expect(createdLinks).toEqual([
55+
{
56+
rel: 'linkset',
57+
href: 'https://staging3.osf.io/metadata/abcde/?format=linkset',
58+
type: 'application/linkset',
59+
},
60+
{
61+
rel: 'linkset',
62+
href: 'https://staging3.osf.io/metadata/abcde/?format=linkset%2Bjson',
63+
type: 'application/linkset+json',
64+
},
65+
]);
66+
expect(mockAppendChild).toHaveBeenCalledTimes(2);
67+
});
68+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { DOCUMENT } from '@angular/common';
2+
import { inject, Injectable, RendererFactory2, RESPONSE_INIT } from '@angular/core';
3+
4+
import { ENVIRONMENT } from '@core/provider/environment.provider';
5+
6+
import { LINKSET_JSON_TYPE, LINKSET_TYPE, SignpostingLink } from '../models/signposting.model';
7+
8+
@Injectable({
9+
providedIn: 'root',
10+
})
11+
export class SignpostingService {
12+
private readonly document = inject(DOCUMENT);
13+
private readonly environment = inject(ENVIRONMENT);
14+
private readonly responseInit = inject(RESPONSE_INIT, { optional: true });
15+
private readonly renderer = inject(RendererFactory2).createRenderer(null, null);
16+
17+
addSignposting(guid: string): void {
18+
const links = this.generateSignpostingLinks(guid);
19+
20+
this.addSignpostingLinkHeaders(links);
21+
this.addSignpostingLinkTags(links);
22+
}
23+
24+
private generateSignpostingLinks(guid: string): SignpostingLink[] {
25+
const baseUrl = `${this.environment.webUrl}/metadata/${guid}/`;
26+
27+
return [
28+
{
29+
rel: 'linkset',
30+
href: this.buildUrl(baseUrl, 'linkset'),
31+
type: LINKSET_TYPE,
32+
},
33+
{
34+
rel: 'linkset',
35+
href: this.buildUrl(baseUrl, 'linkset+json'),
36+
type: LINKSET_JSON_TYPE,
37+
},
38+
];
39+
}
40+
41+
private buildUrl(base: string, format: string): string {
42+
const url = new URL(base);
43+
url.searchParams.set('format', format);
44+
return url.toString();
45+
}
46+
47+
private addSignpostingLinkHeaders(links: SignpostingLink[]): void {
48+
if (!this.responseInit) return;
49+
50+
const headers =
51+
this.responseInit.headers instanceof Headers ? this.responseInit.headers : new Headers(this.responseInit.headers);
52+
53+
const linkHeaderValue = links.map((link) => `<${link.href}>; rel="${link.rel}"; type="${link.type}"`).join(', ');
54+
55+
headers.set('Link', linkHeaderValue);
56+
this.responseInit.headers = headers;
57+
}
58+
59+
private addSignpostingLinkTags(links: SignpostingLink[]): void {
60+
links.forEach((link) => {
61+
const linkElement = this.renderer.createElement('link');
62+
this.renderer.setAttribute(linkElement, 'rel', link.rel);
63+
this.renderer.setAttribute(linkElement, 'href', link.href);
64+
this.renderer.setAttribute(linkElement, 'type', link.type);
65+
this.renderer.appendChild(this.document.head, linkElement);
66+
});
67+
}
68+
}

0 commit comments

Comments
 (0)