Skip to content

Commit 33db68c

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

File tree

3 files changed

+147
-0
lines changed

3 files changed

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

0 commit comments

Comments
 (0)