All files / src/clients/ep jsonLdHelpers.ts

97.61% Statements 123/126
95.2% Branches 139/146
100% Functions 20/20
96.93% Lines 95/98

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300                                          2746x 649x 645x 643x                     1196x 1946x   610x                         1084x 1341x 1341x 937x     147x                       334x 282x 229x 229x   53x 50x 50x 49x 49x     4x                   72x 57x 53x 53x 52x 52x     5x                         299x 299x 306x 302x 302x 302x 302x 7x   4x                       425x 371x 298x 296x 4x 4x 4x                           33x 33x 25x 40x 6x 34x 34x 34x 34x     25x                   141x 22x 1x   21x 4x 4x   17x                   25x 18x 16x 16x 24x 9x 9x 9x     24x                           116x 116x 156x                   114x 574x                       29x 13x 9x                       142x 129x 13x 5x 5x   8x 6x 6x 4x   2x                               50x 46x 43x                   104x 7x   97x    
/**
 * JSON-LD parsing helpers for European Parliament API data.
 *
 * Pure utility functions for extracting and converting values from
 * EP API JSON-LD responses. Used by transform functions and the
 * EP client to safely handle heterogeneous API field formats.
 *
 * @module clients/ep/jsonLdHelpers
 */
 
import type { DocumentType, DocumentStatus } from '../../types/europeanParliament.js';
 
// ─── Primitive helpers ──────────────────────────────────────────
 
/**
 * Safely converts an unknown value to a string.
 *
 * @param value - Value to convert
 * @returns String representation or empty string for unsupported types
 */
export function toSafeString(value: unknown): string {
  if (typeof value === 'string') return value;
  if (typeof value === 'number') return String(value);
  if (typeof value === 'boolean') return String(value);
  return '';
}
 
/**
 * Returns the first non-undefined value from a record, looked up by key list.
 *
 * @param data - Record to search
 * @param keys - Field names to try in order
 * @returns First non-undefined value, or `undefined` if none found
 */
export function firstDefined(data: Record<string, unknown>, ...keys: string[]): unknown {
  for (const k of keys) {
    if (data[k] !== undefined) return data[k];
  }
  return undefined;
}
 
// ─── Field extraction ───────────────────────────────────────────
 
/**
 * Extracts a string value from the first matching field name.
 *
 * @param data - Record to search
 * @param fields - Field names to try in order
 * @returns String value from first matching field, or empty string
 */
export function extractField(data: Record<string, unknown>, fields: string[]): string {
  for (const field of fields) {
    const value = data[field];
    if (value !== undefined && value !== null) {
      return toSafeString(value);
    }
  }
  return '';
}
 
// ─── Date handling ──────────────────────────────────────────────
 
/**
 * Extracts an ISO 8601 date string from various EP API date formats.
 *
 * @param dateField - Raw date field from EP API
 * @returns `YYYY-MM-DD` string, or empty string if not parseable
 */
export function extractDateValue(dateField: unknown): string {
  if (dateField === null || dateField === undefined) return '';
  if (typeof dateField === 'string') {
    const parts = dateField.split('T');
    return parts[0] ?? '';
  }
  if (typeof dateField === 'object' && '@value' in dateField) {
    const val = (dateField as Record<string, unknown>)['@value'];
    if (typeof val === 'string') {
      const parts = val.split('T');
      return parts[0] ?? '';
    }
  }
  return '';
}
 
/**
 * Extracts a date from activity-specific EP API date formats.
 *
 * @param activityDate - Raw activity_date field from EP API
 * @returns `YYYY-MM-DD` string, or empty string
 */
export function extractActivityDate(activityDate: unknown): string {
  if (activityDate === null || activityDate === undefined) return '';
  if (typeof activityDate === 'object' && '@value' in activityDate) {
    const dateValue = (activityDate as Record<string, unknown>)['@value'];
    if (typeof dateValue === 'string') {
      const parts = dateValue.split('T');
      return parts[0] ?? '';
    }
  }
  return '';
}
 
// ─── Multilingual text ──────────────────────────────────────────
 
/**
 * Extracts preferred-language text from an array of language-tagged objects.
 * Prefers English (`en`) or multilingual (`mul`).
 *
 * @param items - Array of JSON-LD language-tagged objects
 * @returns Text in preferred language, or first available
 */
export function extractTextFromLangArray(items: unknown[]): string {
  let fallback = '';
  for (const item of items) {
    if (typeof item !== 'object' || item === null) continue;
    const obj = item as Record<string, unknown>;
    const lang = toSafeString(obj['@language']);
    const value = toSafeString(obj['@value']);
    if (lang === 'en' || lang === 'mul') return value;
    if (fallback === '') fallback = value;
  }
  return fallback;
}
 
/**
 * Extracts a multilingual text value from an EP API JSON-LD field.
 *
 * Handles plain strings, language-tagged objects, and arrays of language variants.
 *
 * @param field - Raw field from EP API
 * @returns Extracted text string, or empty string
 */
export function extractMultilingualText(field: unknown): string {
  if (field === null || field === undefined) return '';
  if (typeof field === 'string') return field;
  if (typeof field === 'number' || typeof field === 'boolean') return String(field);
  if (Array.isArray(field)) return extractTextFromLangArray(field);
  Eif (typeof field === 'object') {
    const obj = field as Record<string, unknown>;
    return toSafeString(obj['en'] ?? obj['@value'] ?? obj['mul']);
  }
  return '';
}
 
// ─── Member / Author extraction ─────────────────────────────────
 
/**
 * Extracts member IDs from EP API membership data.
 *
 * @param memberships - Raw membership field from EP API
 * @returns Array of member ID strings
 */
export function extractMemberIds(memberships: unknown): string[] {
  const members: string[] = [];
  if (!Array.isArray(memberships)) return members;
  for (const m of memberships) {
    if (typeof m === 'string') {
      members.push(m);
    E} else if (typeof m === 'object' && m !== null) {
      const mObj = m as Record<string, unknown>;
      const memberId = toSafeString(mObj['person'] ?? mObj['id'] ?? mObj['@id']);
      if (memberId !== '') members.push(memberId);
    }
  }
  return members;
}
 
/**
 * Extracts author ID from EP API author field.
 *
 * @param authorField - Raw author/attributed-to field
 * @returns Author ID string or empty string
 */
export function extractAuthorId(authorField: unknown): string {
  if (typeof authorField === 'string') return authorField;
  if (Array.isArray(authorField) && authorField.length > 0) {
    return toSafeString(authorField[0]);
  }
  if (typeof authorField === 'object' && authorField !== null) {
    const obj = authorField as Record<string, unknown>;
    return toSafeString(obj['@id'] ?? obj['id']);
  }
  return '';
}
 
/**
 * Extracts document reference strings from EP API document fields.
 *
 * @param docs - Raw document reference field
 * @returns Array of document reference strings
 */
export function extractDocumentRefs(docs: unknown): string[] {
  if (docs === null || docs === undefined) return [];
  if (typeof docs === 'string') return [docs];
  Eif (Array.isArray(docs)) {
    return docs.map(d => {
      if (typeof d === 'string') return d;
      Eif (typeof d === 'object' && d !== null) {
        const obj = d as Record<string, unknown>;
        return toSafeString(obj['id'] ?? obj['identifier'] ?? '');
      }
      return '';
    }).filter(s => s !== '');
  }
  return [];
}
 
// ─── Document type / status mapping ─────────────────────────────
 
/**
 * Maps a raw work-type string to a valid DocumentType.
 *
 * @param rawType - Raw type string from EP API
 * @returns Valid DocumentType
 */
export function mapDocumentType(rawType: string): DocumentType {
  const normalized = (rawType !== '' ? rawType : 'REPORT').replace(/.*\//, '').toUpperCase();
  const validTypes: DocumentType[] = ['REPORT', 'RESOLUTION', 'DECISION', 'DIRECTIVE', 'REGULATION', 'OPINION', 'AMENDMENT'];
  return validTypes.find(t => normalized.includes(t)) ?? 'REPORT';
}
 
/**
 * Maps a raw status string to a valid DocumentStatus.
 *
 * @param rawStatus - Raw status string from EP API
 * @returns Valid DocumentStatus
 */
export function mapDocumentStatus(rawStatus: string): DocumentStatus {
  const validStatuses: DocumentStatus[] = ['DRAFT', 'SUBMITTED', 'IN_COMMITTEE', 'PLENARY', 'ADOPTED', 'REJECTED'];
  return validStatuses.find(s => rawStatus.toUpperCase().includes(s)) ?? 'SUBMITTED';
}
 
// ─── Location extraction ────────────────────────────────────────
 
/**
 * Extracts location string from EP API locality URL.
 *
 * @param localityUrl - Raw locality URL or string
 * @returns Human-readable location string
 */
export function extractLocation(localityUrl: string): string {
  if (localityUrl.includes('FRA_SXB')) return 'Strasbourg';
  if (localityUrl.includes('BEL_BRU')) return 'Brussels';
  return 'Unknown';
}
 
// ─── Vote helpers ───────────────────────────────────────────────
 
/**
 * Extracts a numeric vote count from an EP API value.
 *
 * @param value - Raw vote count value
 * @returns Parsed integer count, or 0
 */
export function extractVoteCount(value: unknown): number {
  if (value === null || value === undefined) return 0;
  if (typeof value === 'number') return value;
  if (typeof value === 'string') {
    const parsed = parseInt(value, 10);
    return isNaN(parsed) ? 0 : parsed;
  }
  if (Array.isArray(value)) return value.length;
  const objValue = (value as Record<string, unknown>)['@value'];
  if (objValue !== undefined) {
    return extractVoteCount(objValue);
  }
  return 0;
}
 
/**
 * Determines vote outcome from EP API decision string.
 *
 * @param decisionStr - Raw decision string from EP API
 * @param votesFor - Number of votes for
 * @param votesAgainst - Number of votes against
 * @returns `'ADOPTED'` or `'REJECTED'`
 */
export function determineVoteOutcome(
  decisionStr: string,
  votesFor: number,
  votesAgainst: number
): 'ADOPTED' | 'REJECTED' {
  if (decisionStr.includes('ADOPTED') || decisionStr.includes('APPROVED')) return 'ADOPTED';
  if (decisionStr.includes('REJECTED')) return 'REJECTED';
  return votesFor >= votesAgainst ? 'ADOPTED' : 'REJECTED';
}
 
/**
 * Maps raw question type string to normalized question type.
 *
 * @param workType - Raw work type string from EP API
 * @returns `'WRITTEN'` or `'ORAL'`
 */
export function mapQuestionType(workType: string): 'WRITTEN' | 'ORAL' {
  if (workType.includes('ORAL') || workType.includes('INTERPELLATION') || workType.includes('QUESTION_TIME')) {
    return 'ORAL';
  }
  return 'WRITTEN';
}