← Files Ship24 Tracking APIARCHIVED FILE

scripts/lib/openapi.mjs

9.91 KB · Oct 2, 2026 · 00:34 UTC

↓ Download file

import { readFile } from 'node:fs/promises';
import { parse } from 'yaml';

const HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];

export async function loadSpec(path) {
  const raw = await readFile(path, 'utf8');
  return parse(raw);
}

function decodeRefSegment(segment) {
  return segment.replace(/~1/g, '/').replace(/~0/g, '~');
}

export function resolveRef(doc, ref) {
  if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
  const segments = ref
    .slice(2)
    .split('/')
    .map((segment) => decodeRefSegment(decodeURIComponent(segment)));
  let node = doc;
  for (const segment of segments) {
    if (node == null || typeof node !== 'object') return null;
    node = node[segment];
  }
  return node ?? null;
}

function refName(ref) {
  if (typeof ref !== 'string') return null;
  const parts = ref.split('/');
  return parts[parts.length - 1] || null;
}

function normalizeTypes(type) {
  if (Array.isArray(type)) return type;
  if (typeof type === 'string') return [type];
  return [];
}

function uniq(list) {
  return [...new Set(list)];
}

function withFormat(schema, base) {
  if (schema && typeof schema.format === 'string' && schema.format.length > 0) {
    return `${base} (${schema.format})`;
  }
  return base;
}

function withNullable(schema, base) {
  if (schema && schema.nullable === true && !/(^|\W)null(\W|$)/.test(base)) {
    return `${base} | null`;
  }
  return base;
}

export function renderType(doc, schema) {
  if (schema == null || typeof schema !== 'object') return 'unknown';

  if (typeof schema.$ref === 'string') {
    return refName(schema.$ref) || 'unknown';
  }

  if (Array.isArray(schema.enum) && schema.enum.length > 0) {
    const base = schema.enum.map((value) => `\`${value}\``).join(' | ');
    return withNullable(schema, withFormat(schema, base));
  }

  const variants = schema.oneOf || schema.anyOf;
  if (Array.isArray(variants) && variants.length > 0) {
    const pieces = uniq(variants.map((variant) => renderType(doc, variant)));
    return withNullable(schema, withFormat(schema, pieces.join(' | ')));
  }

  const types = normalizeTypes(schema.type);
  if (types.length === 0) {
    if (schema.properties) return withNullable(schema, withFormat(schema, 'object'));
    if (schema.items) {
      return withNullable(schema, withFormat(schema, `${renderType(doc, schema.items)}[]`));
    }
    return 'unknown';
  }

  const pieces = [];
  for (const type of types) {
    if (type === 'array') {
      const itemType = schema.items ? renderType(doc, schema.items) : 'unknown';
      const wrapped = itemType.includes(' | ') ? `(${itemType})` : itemType;
      pieces.push(`${wrapped}[]`);
    } else if (type === 'null') {
      pieces.push('null');
    } else {
      pieces.push(type);
    }
  }

  const base = uniq(pieces).join(' | ');
  return withNullable(schema, withFormat(schema, base));
}

export function getSchemaFields(doc, schemaOrRef) {
  let schema = schemaOrRef;
  if (typeof schemaOrRef === 'string') {
    schema = resolveRef(doc, schemaOrRef);
  } else if (schemaOrRef && typeof schemaOrRef.$ref === 'string') {
    schema = resolveRef(doc, schemaOrRef.$ref);
  }
  if (!schema || typeof schema !== 'object' || !schema.properties) return [];

  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
  return Object.entries(schema.properties).map(([name, propSchema]) => ({
    name,
    type: renderType(doc, propSchema),
    required: required.has(name),
    deprecated: Boolean(propSchema && propSchema.deprecated === true),
    description: propSchema?.description || '',
  }));
}

function pickExample(mediaType) {
  if (!mediaType) return null;
  if (mediaType.example !== undefined) return mediaType.example;
  if (mediaType.examples && typeof mediaType.examples === 'object') {
    const first = Object.values(mediaType.examples)[0];
    if (first && typeof first === 'object') {
      if (typeof first.$ref === 'string') return null;
      if ('value' in first) return first.value;
      return first;
    }
  }
  return null;
}

function pickMediaType(content) {
  if (!content || typeof content !== 'object') return null;
  return content['application/json'] || Object.values(content)[0] || null;
}

function extractResponseSchemaName(schema) {
  if (!schema || typeof schema !== 'object') return null;
  if (typeof schema.$ref === 'string') return refName(schema.$ref);
  if (Array.isArray(schema.allOf)) {
    for (const part of schema.allOf) {
      if (part && typeof part.$ref === 'string') return refName(part.$ref);
    }
  }
  return null;
}

function renderParameter(doc, param) {
  return {
    name: param.name,
    in: param.in,
    required: Boolean(param.required),
    type: renderType(doc, param.schema || {}),
    description: param.description || '',
  };
}

function renderRequestBody(doc, requestBody) {
  if (!requestBody || typeof requestBody !== 'object') return null;
  const mediaType = pickMediaType(requestBody.content);
  if (!mediaType) return { schemaName: null, fields: [], example: null };

  const schema = mediaType.schema;
  let schemaName = null;
  let fields = [];
  if (schema && typeof schema.$ref === 'string') {
    schemaName = refName(schema.$ref);
    fields = getSchemaFields(doc, schema.$ref);
  } else if (schema) {
    fields = getSchemaFields(doc, schema);
  }

  return { schemaName, fields, example: pickExample(mediaType) };
}

function renderResponses(responses) {
  if (!responses || typeof responses !== 'object') return [];
  return Object.entries(responses).map(([status, response]) => {
    const mediaType = pickMediaType(response?.content);
    const schemaName = mediaType ? extractResponseSchemaName(mediaType.schema) : null;
    return {
      status,
      description: response?.description || '',
      schemaName,
    };
  });
}

function stripPublicV1Prefix(path) {
  const prefix = '/public/v1';
  if (path.startsWith(prefix)) {
    const rest = path.slice(prefix.length);
    return rest === '' ? '/' : rest;
  }
  return path;
}

export function getOperations(doc) {
  const paths = doc?.paths || {};
  const operations = [];

  for (const [rawPath, pathItem] of Object.entries(paths)) {
    if (!pathItem || typeof pathItem !== 'object') continue;
    const pathLevelParams = Array.isArray(pathItem.parameters) ? pathItem.parameters : [];

    for (const method of Object.keys(pathItem)) {
      if (!HTTP_METHODS.includes(method)) continue;
      const operation = pathItem[method];
      if (!operation || typeof operation !== 'object') continue;

      const operationParams = Array.isArray(operation.parameters) ? operation.parameters : [];
      const parameters = [...pathLevelParams, ...operationParams].map((param) =>
        renderParameter(doc, param),
      );

      operations.push({
        method: method.toUpperCase(),
        path: stripPublicV1Prefix(rawPath),
        operationId: operation.operationId || null,
        summary: operation.summary || '',
        description: operation.description || '',
        tags: Array.isArray(operation.tags) ? operation.tags : [],
        parameters,
        requestBody: renderRequestBody(doc, operation.requestBody),
        responses: renderResponses(operation.responses),
      });
    }
  }

  return operations;
}

function flattenTrackingItemFields(doc, trackingsSchema) {
  if (!trackingsSchema?.items) return [];
  return getSchemaFields(doc, trackingsSchema.items).map((field) => ({
    ...field,
    name: `trackings[].${field.name}`,
  }));
}

function buildMinimalExample(doc, schema, depth = 0) {
  if (!schema || typeof schema !== 'object' || depth > 6) return null;
  if (typeof schema.$ref === 'string') {
    return buildMinimalExample(doc, resolveRef(doc, schema.$ref), depth + 1);
  }
  if (schema.example !== undefined) return schema.example;
  if (Array.isArray(schema.examples) && schema.examples.length > 0) return schema.examples[0];
  if (schema.default !== undefined) return schema.default;

  const types = normalizeTypes(schema.type);
  if (types.includes('array')) {
    const item = buildMinimalExample(doc, schema.items, depth + 1);
    return item === undefined ? [] : [item];
  }
  if (schema.properties) {
    const out = {};
    for (const [key, propSchema] of Object.entries(schema.properties)) {
      out[key] = buildMinimalExample(doc, propSchema, depth + 1);
    }
    return out;
  }
  if (types.includes('string')) return '';
  if (types.includes('integer') || types.includes('number')) return 0;
  if (types.includes('boolean')) return false;
  return null;
}

const WEBHOOK_TOPICS = {
  'receive-webhooks-tracking-results': 'tracking/events',
  'receive-webhooks-proof-of-delivery': 'tracking/pod',
};

export function getWebhookPayloads(doc) {
  const webhooks = doc?.webhooks || {};
  const results = [];

  for (const pathItem of Object.values(webhooks)) {
    if (!pathItem || typeof pathItem !== 'object') continue;
    for (const method of Object.keys(pathItem)) {
      if (!HTTP_METHODS.includes(method)) continue;
      const operation = pathItem[method];
      if (!operation || typeof operation !== 'object') continue;
      const operationId = operation.operationId;
      if (!operationId || !(operationId in WEBHOOK_TOPICS)) continue;

      const mediaType = pickMediaType(operation.requestBody?.content);
      const schema = mediaType?.schema;
      const trackingsSchema = schema?.properties?.trackings;
      const fields = flattenTrackingItemFields(doc, trackingsSchema);

      let example = mediaType ? pickExample(mediaType) : null;
      let note;
      if (!example) {
        example = buildMinimalExample(doc, schema);
        note =
          'No example found in the spec; this is a minimal example built from schema defaults/examples.';
      }

      const payload = {
        operationId,
        summary: operation.summary || '',
        description: operation.description || '',
        topic: WEBHOOK_TOPICS[operationId],
        fields,
        example,
      };
      if (note) payload.note = note;
      results.push(payload);
    }
  }

  return results;
}

SHA-256: f4e84547caa5a3a72de8a910a93e4e65dd8f781b11a5c661924c0f7edbddeced