📄 src/issn/index.ts
D-OPEN SOVEREIGN
1/*
2 * Copyright (c) 2026 DataPond D-Library Pty Ltd. ("The Sanctuary")
3 * Non-Profit Public Library — The World Library
4 * 
5 * D-SAFE Certification issued by POND Enterprise.
6 * Published under the D-Open Code Sovereign Licence v1.0 framework
7 * DataPond D-Library All exclusive Right reserved on modifiying the code - CC Attribution to data pond, Non modifiable, Non Commercial.
8 * https://registry.world.bibliotech.com/licence
9 * Source code donated to DataPond D-Library by it's author: data pond.
10 * 
11 * Technical Guardian ("The Shield"): Pond Enterprise Pty Ltd. (ACN 694 747 987)
12 * All liability claims about D-SAFE certification issues coming from the published D-Safe direction must be addressed with Pond Enterprise Pty Ltd.
13 * Code Modification automatically voids the certified liability protection provided by Pond Enterprise.
14 */
15import type { StandardAdapter, StandardApiAdapter, ValidationResult }
16  from '@the_library/canonical-schemas/types/StandardAdapter';
17import type { IssnPortalResult } from './types';
18
19interface ISSNProps extends Record<string, string | null> { issn: string | null; }
20
21type ISSNApis = {
22  'issn-portal': StandardApiAdapter<ISSNProps, IssnPortalResult>;
23};
24
25function validateISSNMod11_2(issn: string): ValidationResult {
26  const clean = issn.replace('-', '');
27  if (clean.length !== 8) return { valid: false, error: `Expected 8 chars, got ${clean.length}` };
28  const weights = [8, 7, 6, 5, 4, 3, 2];
29  let sum = 0;
30  for (let i = 0; i < 7; i++) sum += parseInt(clean[i], 10) * weights[i];
31  const remainder = sum % 11;
32  const checkVal  = remainder === 0 ? 0 : 11 - remainder;
33  const checkChar = checkVal === 10 ? 'X' : String(checkVal);
34  if (clean[7].toUpperCase() !== checkChar)
35    return { valid: false, error: `Check digit mismatch: expected ${checkChar}, got ${clean[7]}` };
36  return { valid: true };
37}
38
39export const ISSNAdapter: StandardAdapter<ISSNProps, ISSNApis> = {
40  standardId: 'issn',
41  uriScheme:  'ISSN',
42
43  normalize(value: string): string {
44    const digits = value.replace(/[\s-]/g, '');
45    if (digits.length === 8) return `${digits.slice(0, 4)}-${digits.slice(4)}`;
46    return value.trim();
47  },
48
49  display(value: string): string {
50    return ISSNAdapter.normalize(value);
51  },
52
53  validate(value: string | null | undefined): ValidationResult {
54    if (!value) return { valid: false, error: 'ISSN is absent' };
55    const normalized = ISSNAdapter.normalize(value);
56    if (!/^[0-9]{4}-[0-9]{3}[0-9X]$/.test(normalized))
57      return { valid: false, error: `ISSN does not match pattern: ${normalized}` };
58    return validateISSNMod11_2(normalized);
59  },
60
61  toLinkedDataUri(value: string): string {
62    return `https://portal.issn.org/resource/ISSN/${ISSNAdapter.normalize(value)}`;
63  },
64
65  apis: {
66    'issn-portal': {
67      apiId: 'issn-portal',
68      name:  'ISSN Portal',
69      async fetch({ issn }): Promise<IssnPortalResult> {
70        if (!issn) return {};
71        const res = await fetch(`https://portal.issn.org/resource/ISSN/${issn}?format=json`);
72        if (!res.ok) return {};
73        const data = await res.json() as Record<string, unknown>;
74        return {
75          title:   data['dc:title']     as string | undefined,
76          medium:  data['dc:format']    as string | undefined,
77          country: data['dc:publisher'] as string | undefined,
78          issn,
79        };
80      },
81    },
82  },
83};
84