{"format":1,"manifest":{"name":"api-tools","publisher":"vyloxi","version":"1.0.0","displayName":"API Tools","description":"An OpenAPI / Swagger viewer for the spec in your project (JSON or YAML) and a GraphQL playground.","engines":{"ferrum":"^0.1.0"},"categories":["Tools"],"icon":"icon.png","repository":"https://github.com/voukengdongmofrankysteve/ferrum-market","license":"MIT","main":"extension.js","activationEvents":["onCommand:apiTools.openApiViewer","onCommand:apiTools.listEndpoints","onCommand:apiTools.graphqlPlayground"],"permissions":["workspace.read","network"],"contributes":{"commands":[{"command":"apiTools.openApiViewer","title":"View the OpenAPI Description","category":"API Tools"},{"command":"apiTools.listEndpoints","title":"List Endpoints...","category":"API Tools"},{"command":"apiTools.graphqlPlayground","title":"Open GraphQL Playground","category":"API Tools"}],"configuration":{"title":"API Tools","properties":{"apiTools.graphqlEndpoint":{"type":"string","default":"","description":"The GraphQL address the playground starts with, for example https://api.example.com/graphql."}}}}},"files":{"icon.png":{"encoding":"base64","content":"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"},"extension.js":{"encoding":"utf8","content":"// API Tools: an OpenAPI / Swagger viewer for the spec in your project (JSON or YAML) and a GraphQL playground that sends queries to\n// an endpoint you choose. Reads your files and talks to the endpoint you type; nothing else is contacted.\n// (A WebSocket tester is not possible: Ferrum extensions run in a worker that has no WebSocket.)\n\n// ---- a small YAML reader: the subset OpenAPI files use ---------------------------------------------------------------\nfunction stripComment(line) {\n  let q = '';\n  for (let i = 0; i < line.length; i++) {\n    const c = line[i];\n    if (q) { if (c === '\\\\' && q === '\"') i++; else if (c === q) q = ''; } else if (c === '\"' || c === \"'\") { if (i === 0 || /[\\s:,[{(-]/.test(line[i - 1])) q = c; } else if (c === '#' && (i === 0 || /\\s/.test(line[i - 1]))) return line.slice(0, i);\n  }\n  return line;\n}\n\nfunction scalar(s) {\n  const t = s.trim();\n  if (t === '' || t === '~' || t === 'null') return null;\n  if (t === 'true' || t === 'True') return true;\n  if (t === 'false' || t === 'False') return false;\n  if (/^[-+]?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?$/.test(t)) return Number(t);\n  if (t.startsWith('\"') && t.endsWith('\"') && t.length >= 2) { try { return JSON.parse(t); } catch { return t.slice(1, -1); } }\n  if (t.startsWith(\"'\") && t.endsWith(\"'\") && t.length >= 2) return t.slice(1, -1).replace(/''/g, \"'\");\n  if (t.startsWith('[') || t.startsWith('{')) return flow(t);\n  return t;\n}\n\n/** Flow collections: [a, b, {c: d}] and {a: 1, b: [x]}. */\nfunction flow(text) {\n  let i = 0;\n  const ws = () => { while (i < text.length && /\\s/.test(text[i])) i++; };\n  function value() {\n    ws();\n    if (text[i] === '[') {\n      i++;\n      const arr = [];\n      ws();\n      while (i < text.length && text[i] !== ']') { arr.push(value()); ws(); if (text[i] === ',') i++; ws(); }\n      i++;\n      return arr;\n    }\n    if (text[i] === '{') {\n      i++;\n      const obj = {};\n      ws();\n      while (i < text.length && text[i] !== '}') {\n        const k = token(':');\n        i++;\n        obj[String(scalar(k))] = value();\n        ws();\n        if (text[i] === ',') i++;\n        ws();\n      }\n      i++;\n      return obj;\n    }\n    return scalar(token(',]}'));\n  }\n  function token(stops) {\n    ws();\n    const start = i;\n    if (text[i] === '\"' || text[i] === \"'\") {\n      const q = text[i++];\n      while (i < text.length && text[i] !== q) { if (text[i] === '\\\\' && q === '\"') i++; i++; }\n      i++;\n      return text.slice(start, i);\n    }\n    while (i < text.length && !stops.includes(text[i])) i++;\n    return text.slice(start, i).trim();\n  }\n  return value();\n}\n\n/** Splits \"key: value\" at the first colon that is followed by a space or the end, outside quotes. */\nfunction splitKey(text) {\n  let q = '';\n  for (let i = 0; i < text.length; i++) {\n    const c = text[i];\n    if (q) { if (c === q) q = ''; continue; }\n    if (c === '\"' || c === \"'\") { if (i === 0) q = c; continue; }\n    if (c === ':' && (i === text.length - 1 || text[i + 1] === ' ')) return [text.slice(0, i).trim(), text.slice(i + 1).trim()];\n  }\n  return null;\n}\n\nfunction parseYaml(src) {\n  const rawLines = String(src).replace(/\\r\\n/g, '\\n').replace(/\\t/g, '  ').split('\\n');\n  const lines = rawLines.map((raw) => {\n    const stripped = stripComment(raw);\n    return { raw, indent: /^ */.exec(raw)[0].length, text: stripped.trim(), blank: stripped.trim() === '' };\n  });\n  let p = 0;\n  const skip = () => { while (p < lines.length && lines[p].blank) p++; };\n  const key = (k) => { const v = scalar(k); return typeof v === 'string' ? v : String(v); };\n\n  function node(indent) {\n    skip();\n    if (p >= lines.length) return null;\n    const l = lines[p];\n    if (l.text.startsWith('- ') || l.text === '-') return seq(l.indent);\n    return map(l.indent);\n  }\n\n  function blockScalar(parentIndent, style) {\n    const out = [];\n    let ind = -1;\n    while (p < lines.length) {\n      const l = lines[p];\n      const isBlank = l.raw.trim() === '';\n      if (!isBlank && l.indent <= parentIndent) break;\n      if (!isBlank && ind < 0) ind = l.indent;\n      out.push(isBlank ? '' : l.raw.slice(ind));\n      p++;\n    }\n    while (out.length && out[out.length - 1] === '') out.pop();\n    return style.startsWith('|') ? out.join('\\n') + '\\n' : out.join(' ').replace(/ {2,}/g, ' ') + '\\n';\n  }\n\n  function valueAfter(rest, indent) {\n    if (rest === '|' || rest === '>' || /^[|>][+-]?$/.test(rest)) { p++; return blockScalar(indent, rest).replace(/\\n$/, rest.endsWith('-') ? '' : '\\n'); }\n    if (rest === '') {\n      p++;\n      skip();\n      if (p < lines.length) {\n        const n = lines[p];\n        if (n.indent > indent) return node(n.indent);\n        if (n.indent === indent && (n.text.startsWith('- ') || n.text === '-')) return seq(indent);\n      }\n      return null;\n    }\n    p++;\n    return scalar(rest);\n  }\n\n  function map(indent) {\n    const o = {};\n    for (;;) {\n      skip();\n      if (p >= lines.length || lines[p].indent !== indent || lines[p].text.startsWith('- ') || lines[p].text === '-') break;\n      const kv = splitKey(lines[p].text);\n      if (!kv) { p++; continue; }\n      o[key(kv[0])] = valueAfter(kv[1], indent);\n    }\n    return o;\n  }\n\n  function seq(indent) {\n    const a = [];\n    for (;;) {\n      skip();\n      if (p >= lines.length || lines[p].indent !== indent || !(lines[p].text.startsWith('- ') || lines[p].text === '-')) break;\n      const rest = lines[p].text === '-' ? '' : lines[p].text.slice(2).trim();\n      if (rest === '') {\n        p++;\n        skip();\n        a.push(p < lines.length && lines[p].indent > indent ? node(lines[p].indent) : null);\n      } else if (splitKey(rest) && !rest.startsWith('{') && !rest.startsWith('[') && !rest.startsWith('\"')) {\n        // \"- key: value\": the rest of the line starts a mapping two columns in.\n        const offset = lines[p].text.length - rest.length;\n        lines[p] = { raw: ' '.repeat(indent + offset) + rest, indent: indent + offset, text: rest, blank: false };\n        a.push(map(indent + offset));\n      } else {\n        p++;\n        a.push(scalar(rest));\n      }\n    }\n    return a;\n  }\n\n  skip();\n  if (p >= lines.length) return null;\n  if (lines[p].text === '---') p++;\n  return node(lines[Math.min(p, lines.length - 1)].indent);\n}\n\n// ---- OpenAPI -----------------------------------------------------------------------------------------------------------\nconst METHODS = ['get', 'post', 'put', 'patch', 'delete', 'options', 'head', 'trace'];\nconst refName = (r) => String(r || '').split('/').pop();\n\nfunction resolve(spec, node, depth = 0) {\n  if (node && typeof node === 'object' && typeof node.$ref === 'string' && node.$ref.startsWith('#/') && depth < 6) {\n    let cur = spec;\n    for (const part of node.$ref.slice(2).split('/')) cur = cur && cur[part.replace(/~1/g, '/').replace(/~0/g, '~')];\n    return cur ? resolve(spec, cur, depth + 1) : node;\n  }\n  return node;\n}\n\n/** A short text for a schema: string, integer(int64), array<User>, User, enum a|b. */\nfunction schemaText(spec, s, depth = 0) {\n  if (!s || typeof s !== 'object') return 'any';\n  if (s.$ref) return refName(s.$ref);\n  if (s.enum) return (s.type || 'enum') + ' ' + s.enum.slice(0, 6).join(' | ') + (s.enum.length > 6 ? ' ...' : '');\n  if (s.type === 'array') return 'array<' + (depth < 3 ? schemaText(spec, s.items, depth + 1) : '...') + '>';\n  if (s.oneOf) return s.oneOf.map((x) => schemaText(spec, x, depth + 1)).join(' | ');\n  if (s.anyOf) return s.anyOf.map((x) => schemaText(spec, x, depth + 1)).join(' | ');\n  if (s.allOf) return s.allOf.map((x) => schemaText(spec, x, depth + 1)).join(' + ');\n  if (s.type === 'object' || s.properties) return s.properties ? 'object{' + Object.keys(s.properties).slice(0, 6).join(', ') + (Object.keys(s.properties).length > 6 ? ', ...' : '') + '}' : 'object';\n  return (s.type || 'any') + (s.format ? '(' + s.format + ')' : '');\n}\n\nfunction summarize(spec) {\n  if (!spec || typeof spec !== 'object' || !spec.paths) throw new Error('This does not look like an OpenAPI or Swagger file: it has no \"paths\".');\n  const info = spec.info || {};\n  const servers = (spec.servers || []).map((s) => s.url).filter(Boolean);\n  if (!servers.length && spec.host) servers.push((spec.schemes && spec.schemes[0] ? spec.schemes[0] : 'https') + '://' + spec.host + (spec.basePath || ''));\n  const ops = [];\n  for (const [path, item0] of Object.entries(spec.paths)) {\n    const item = resolve(spec, item0) || {};\n    for (const m of METHODS) {\n      const op = item[m];\n      if (!op) continue;\n      const params = [...(item.parameters || []), ...(op.parameters || [])].map((x) => resolve(spec, x)).filter(Boolean).map((x) => ({ name: x.name, in: x.in, required: !!x.required, type: schemaText(spec, x.schema || { type: x.type }), description: x.description || '' }));\n      let body = null;\n      const rb = resolve(spec, op.requestBody);\n      if (rb && rb.content) body = Object.entries(rb.content).map(([type, c]) => ({ type, schema: schemaText(spec, c.schema) }));\n      else if (Array.isArray(op.consumes) && params.some((x) => x.in === 'body')) body = op.consumes.map((type) => ({ type, schema: params.find((x) => x.in === 'body').type }));\n      const responses = Object.entries(op.responses || {}).map(([code, r0]) => {\n        const r = resolve(spec, r0) || {};\n        const first = r.content ? Object.values(r.content)[0] : null;\n        return { code, description: r.description || '', schema: first && first.schema ? schemaText(spec, first.schema) : r.schema ? schemaText(spec, r.schema) : '' };\n      });\n      ops.push({ method: m.toUpperCase(), path, summary: op.summary || '', description: op.description || '', tags: op.tags && op.tags.length ? op.tags : ['default'], id: op.operationId || '', deprecated: !!op.deprecated, params, body, responses, secured: !!(op.security && op.security.length) || !!(spec.security && spec.security.length) });\n    }\n  }\n  const schemas = Object.entries((spec.components && spec.components.schemas) || spec.definitions || {}).map(([name, s0]) => {\n    const s = resolve(spec, s0) || {};\n    const req = new Set(s.required || []);\n    return { name, type: s.type || 'object', description: s.description || '', props: Object.entries(s.properties || {}).map(([k, v]) => ({ name: k, type: schemaText(spec, v), required: req.has(k) })) };\n  });\n  return { title: info.title || 'API', version: info.version || '', description: info.description || '', servers, ops, schemas, tags: [...new Set(ops.flatMap((o) => o.tags))] };\n}\n\nconst esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\nconst COLORS = { GET: '#3b82f6', POST: '#22c55e', PUT: '#f59e0b', PATCH: '#a855f7', DELETE: '#ef4444', OPTIONS: '#64748b', HEAD: '#64748b', TRACE: '#64748b' };\n\nfunction viewerPage(s) {\n  const groups = s.tags.map((t) => {\n    const ops = s.ops.filter((o) => o.tags.includes(t));\n    return `<h3 class=\"tag\">${esc(t)} <span class=\"dim\">${ops.length}</span></h3>${ops.map((o) => {\n      const params = o.params.length ? `<h5>Parameters</h5><table>${o.params.map((p) => `<tr><td><code>${esc(p.name)}</code>${p.required ? ' <b class=\"req\">required</b>' : ''}</td><td>${esc(p.in)}</td><td><code>${esc(p.type)}</code></td><td>${esc(p.description)}</td></tr>`).join('')}</table>` : '';\n      const body = o.body ? `<h5>Request body</h5><table>${o.body.map((b) => `<tr><td><code>${esc(b.type)}</code></td><td><code>${esc(b.schema)}</code></td></tr>`).join('')}</table>` : '';\n      const resp = o.responses.length ? `<h5>Responses</h5><table>${o.responses.map((r) => `<tr><td><b class=\"code c${esc(String(r.code)[0])}\">${esc(r.code)}</b></td><td>${esc(r.description)}</td><td><code>${esc(r.schema)}</code></td></tr>`).join('')}</table>` : '';\n      return `<details class=\"op\" data-q=\"${esc((o.method + ' ' + o.path + ' ' + o.summary + ' ' + o.tags.join(' ') + ' ' + o.id).toLowerCase())}\"><summary><span class=\"m\" style=\"background:${COLORS[o.method] || '#64748b'}\">${o.method}</span><code class=\"p${o.deprecated ? ' dep' : ''}\">${esc(o.path)}</code><span class=\"sum\">${esc(o.summary)}</span>${o.secured ? '<span title=\"Needs authorization\">🔒</span>' : ''}</summary><div class=\"body\">${o.description ? `<p>${esc(o.description)}</p>` : ''}${params}${body}${resp}<button class=\"cp\" data-c=\"${esc(o.method + ' ' + o.path)}\">Insert \"${esc(o.method + ' ' + o.path)}\"</button></div></details>`;\n    }).join('')}`;\n  }).join('');\n  const schemas = s.schemas.length ? `<h3 class=\"tag\">Schemas <span class=\"dim\">${s.schemas.length}</span></h3>${s.schemas.map((x) => `<details class=\"op\" data-q=\"${esc(('schema ' + x.name).toLowerCase())}\"><summary><span class=\"m\" style=\"background:#0ea5e9\">${esc(x.type)}</span><code class=\"p\">${esc(x.name)}</code><span class=\"sum\">${esc(x.description)}</span></summary><div class=\"body\">${x.props.length ? `<table>${x.props.map((p) => `<tr><td><code>${esc(p.name)}</code>${p.required ? ' <b class=\"req\">required</b>' : ''}</td><td><code>${esc(p.type)}</code></td></tr>`).join('')}</table>` : '<p class=\"dim\">No properties.</p>'}</div></details>`).join('')}` : '';\n  return `<style>\n  body { font-family: var(--ui); } h2 { margin: 0; } .dim { color: var(--fg-dim); font-size: 12px; font-weight: 400; } h3.tag { margin: 22px 0 8px; font-size: 14px; } h5 { margin: 12px 0 4px; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--fg-dim); }\n  input { width: 100%; box-sizing: border-box; background: var(--raised, transparent); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; font: 14px var(--ui); outline: none; margin: 12px 0 0; } input:focus { border-color: var(--accent); }\n  details.op { border: 1px solid var(--border); border-radius: 7px; margin-bottom: 6px; } summary { display: flex; gap: 10px; align-items: center; padding: 7px 10px; cursor: pointer; }\n  .m { color: #fff; font: 700 11px var(--mono, monospace); padding: 2px 7px; border-radius: 4px; min-width: 46px; text-align: center; } .p { font: 13px var(--mono, monospace); } .p.dep { text-decoration: line-through; opacity: .6; } .sum { color: var(--fg-dim); font-size: 13px; flex: 1; }\n  .body { padding: 2px 14px 12px; } table { border-collapse: collapse; width: 100%; font-size: 12.5px; } td { padding: 3px 8px 3px 0; vertical-align: top; border-top: 1px solid var(--border); } code { font: 12px var(--mono, monospace); }\n  .req { color: #ef4444; font-size: 10px; } .code { font: 700 12px var(--mono, monospace); } .c2 { color: #22c55e; } .c3 { color: #3b82f6; } .c4 { color: #f59e0b; } .c5 { color: #ef4444; }\n  button { background: var(--raised, transparent); color: var(--fg); border: 1px solid var(--border); border-radius: 5px; padding: 4px 10px; cursor: pointer; margin-top: 10px; } button:hover { border-color: var(--accent); }\n</style>\n<h2>${esc(s.title)} <span class=\"dim\">${esc(s.version)}</span></h2>\n${s.description ? `<p class=\"dim\">${esc(s.description.slice(0, 400))}</p>` : ''}\n${s.servers.length ? `<div class=\"dim\">Servers: ${s.servers.map((x) => `<code>${esc(x)}</code>`).join(', ')}</div>` : ''}\n<div class=\"dim\">${s.ops.length} operations in ${s.tags.length} group${s.tags.length === 1 ? '' : 's'}, ${s.schemas.length} schemas</div>\n<input id=\"q\" placeholder=\"Search paths, methods, tags, summaries\" autofocus>\n<div id=\"all\">${groups}${schemas}</div>\n<script>\n  const q = document.getElementById('q');\n  q.addEventListener('input', () => { const s = q.value.trim().toLowerCase(); document.querySelectorAll('details.op').forEach((d) => { d.style.display = !s || d.dataset.q.includes(s) ? '' : 'none'; if (s && d.dataset.q.includes(s)) d.open = false; }); });\n  document.getElementById('all').addEventListener('click', (e) => { const b = e.target.closest('.cp'); if (b) ferrumPanel.postMessage({ type: 'insert', text: b.dataset.c }); });\n</script>`;\n}\n\n// ---- GraphQL playground -------------------------------------------------------------------------------------------------\nconst INTROSPECTION = 'query { __schema { queryType { name } mutationType { name } types { name kind description fields { name args { name type { name kind ofType { name kind } } } type { name kind ofType { name kind ofType { name kind } } } } } } }';\n\nfunction typeText(t) {\n  if (!t) return 'any';\n  if (t.kind === 'NON_NULL') return typeText(t.ofType) + '!';\n  if (t.kind === 'LIST') return '[' + typeText(t.ofType) + ']';\n  return t.name || 'any';\n}\n\n/** The queries and mutations of an introspection result, as lines like \"user(id: ID!): User\". */\nfunction schemaOutline(result) {\n  const s = result && result.data && result.data.__schema;\n  if (!s) throw new Error('The server did not return a schema. Introspection may be turned off.');\n  const byName = Object.fromEntries(s.types.map((t) => [t.name, t]));\n  const lines = [];\n  for (const [label, root] of [['Query', s.queryType], ['Mutation', s.mutationType]]) {\n    const t = root && byName[root.name];\n    if (!t || !t.fields) continue;\n    lines.push('# ' + label);\n    for (const f of t.fields) lines.push(f.name + (f.args && f.args.length ? '(' + f.args.map((a) => a.name + ': ' + typeText(a.type)).join(', ') + ')' : '') + ': ' + typeText(f.type));\n    lines.push('');\n  }\n  const types = s.types.filter((t) => !t.name.startsWith('__') && t.kind === 'OBJECT' && (!s.queryType || t.name !== s.queryType.name) && (!s.mutationType || t.name !== s.mutationType.name));\n  if (types.length) lines.push('# Types', types.map((t) => t.name + ' { ' + (t.fields || []).map((f) => f.name).join(', ') + ' }').join('\\n'));\n  return lines.join('\\n');\n}\n\nconst validUrl = (u) => { try { const x = new URL(u); return (x.protocol === 'http:' || x.protocol === 'https:') ? x : null; } catch { return null; } };\n\nfunction playgroundPage(init) {\n  return `<style>\n  body { font-family: var(--ui); } h3 { margin: 0 0 8px; } label { display: block; font-size: 12px; color: var(--fg-dim); margin: 10px 0 3px; }\n  input, textarea { width: 100%; box-sizing: border-box; background: var(--raised, transparent); color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 7px 9px; font: 12.5px/1.5 var(--mono, monospace); outline: none; } input:focus, textarea:focus { border-color: var(--accent); }\n  textarea { resize: vertical; min-height: 120px; } .row { display: flex; gap: 8px; align-items: center; margin-top: 10px; } .row .sp { flex: 1; }\n  button { background: var(--raised, transparent); color: var(--fg); border: 1px solid var(--border); border-radius: 5px; padding: 6px 14px; cursor: pointer; } button:hover { border-color: var(--accent); } button.go { background: var(--accent); color: #fff; border-color: var(--accent); }\n  .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } @media (max-width: 760px) { .cols { grid-template-columns: 1fr; } }\n  pre { font: 12.5px/1.55 var(--mono, monospace); background: var(--raised, rgba(128,128,128,.12)); border-radius: 6px; padding: 10px 12px; overflow: auto; margin: 0; min-height: 260px; max-height: 70vh; white-space: pre-wrap; word-break: break-word; }\n  .dim { color: var(--fg-dim); font-size: 12px; } .ok { color: #22c55e; } .bad { color: #ef4444; }\n</style>\n<h3>GraphQL playground</h3>\n<label>Endpoint</label><input id=\"ep\" value=\"${esc(init.endpoint)}\" placeholder=\"https://api.example.com/graphql\">\n<div class=\"cols\">\n  <div>\n    <label>Query</label><textarea id=\"qy\" spellcheck=\"false\">${esc(init.query)}</textarea>\n    <label>Variables (JSON)</label><textarea id=\"vr\" spellcheck=\"false\" style=\"min-height:70px\">${esc(init.variables)}</textarea>\n    <label>Headers (JSON, not saved). For example {\"Authorization\": \"Bearer ...\"}</label><textarea id=\"hd\" spellcheck=\"false\" style=\"min-height:60px\"></textarea>\n    <div class=\"row\"><button class=\"go\" id=\"run\">Run query</button><button id=\"sc\">Show schema</button><span class=\"sp\"></span><span class=\"dim\" id=\"st\"></span></div>\n  </div>\n  <div><label>Response</label><pre id=\"out\">Press Run query.</pre></div>\n</div>\n<script>\n  const $ = (id) => document.getElementById(id);\n  const get = () => ({ endpoint: $('ep').value.trim(), query: $('qy').value, variables: $('vr').value, headers: $('hd').value });\n  $('run').onclick = () => { $('st').textContent = 'Running...'; ferrumPanel.postMessage({ type: 'run', ...get() }); };\n  $('sc').onclick = () => { $('st').textContent = 'Loading schema...'; ferrumPanel.postMessage({ type: 'schema', ...get() }); };\n  ferrumPanel.onMessage((m) => { if (m.type === 'result') { $('out').textContent = m.text; $('st').innerHTML = '<span class=\"' + (m.ok ? 'ok' : 'bad') + '\">' + m.status + '</span> ' + m.ms + ' ms'; } });\n</script>`;\n}\n\nexports._test = { parseYaml, flow, scalar, summarize, schemaText, resolve, schemaOutline, typeText, validUrl, refName };\n\nexports.activate = function (context) {\n  let viewer = null;\n  let play = null;\n  const reg = (id, fn) => context.subscriptions.push(ferrum.commands.register(id, fn));\n\n  function parseSpec(name, text) {\n    const t = text.trim();\n    if (!t) throw new Error('The file is empty.');\n    return /\\.json$/i.test(name) || t.startsWith('{') ? JSON.parse(t) : parseYaml(t);\n  }\n\n  async function findSpec() {\n    const doc = await ferrum.editor.active();\n    if (doc && /(openapi|swagger)[\"']?\\s*:/.test(doc.text.slice(0, 4000)) && /\\.(json|ya?ml)$/i.test(doc.name)) return { name: doc.name, text: doc.text };\n    if (!ferrum.workspace.root) throw new Error('Open an OpenAPI file, or a folder that has one.');\n    const files = (await ferrum.workspace.listFiles()).filter((f) => /(openapi|swagger)[^/]*\\.(json|ya?ml)$/i.test(f) || /(^|\\/)(api|spec)[^/]*\\.(json|ya?ml)$/i.test(f));\n    if (!files.length) throw new Error('No OpenAPI file found. Open one in the editor and run this again.');\n    let file = files[0];\n    if (files.length > 1) {\n      const pick = await ferrum.window.showQuickPick(files.map((f) => ({ label: f, f })), { placeholder: 'Which API description?' });\n      if (!pick) return null;\n      file = pick.f;\n    }\n    return { name: file, text: await ferrum.workspace.readFile(file) };\n  }\n\n  reg('apiTools.openApiViewer', async () => {\n    try {\n      const found = await findSpec();\n      if (!found) return;\n      const html = viewerPage(summarize(parseSpec(found.name, found.text)));\n      if (viewer) return viewer.setHtml(html);\n      viewer = await ferrum.window.showPanel({ title: 'API: ' + found.name.split('/').pop(), html });\n      viewer.onDidClose(() => (viewer = null));\n      viewer.onMessage(async (m) => {\n        if (m.type !== 'insert') return;\n        const d = await ferrum.editor.active();\n        if (d) await ferrum.editor.insert(String(m.text || '').slice(0, 300));\n      });\n    } catch (e) {\n      ferrum.window.showMessage('Cannot show the API: ' + e.message, 'warn');\n    }\n  });\n\n  reg('apiTools.listEndpoints', async () => {\n    try {\n      const found = await findSpec();\n      if (!found) return;\n      const s = summarize(parseSpec(found.name, found.text));\n      const pick = await ferrum.window.showQuickPick(s.ops.map((o) => ({ label: o.method + ' ' + o.path, detail: o.summary, description: o.tags.join(', ') })), { placeholder: s.ops.length + ' endpoints. Pick one to insert it.' });\n      if (pick) await ferrum.editor.insert(pick.label);\n    } catch (e) {\n      ferrum.window.showMessage('Cannot read the API: ' + e.message, 'warn');\n    }\n  });\n\n  async function send(m, body) {\n    const url = validUrl(m.endpoint);\n    if (!url) throw new Error('The endpoint must be an http or https address.');\n    let headers = {};\n    if (m.headers && m.headers.trim()) {\n      try { headers = JSON.parse(m.headers); } catch { throw new Error('The headers are not valid JSON.'); }\n      if (!headers || typeof headers !== 'object' || Array.isArray(headers)) throw new Error('The headers must be a JSON object.');\n    }\n    const t0 = Date.now();\n    const res = await ferrum.network.fetch(url.href, { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json', ...headers }, body: JSON.stringify(body) });\n    const text = await res.text();\n    return { res, text, ms: Date.now() - t0 };\n  }\n\n  reg('apiTools.graphqlPlayground', async () => {\n    if (play) return;\n    const doc = await ferrum.editor.active();\n    const saved = await ferrum.storage.get('graphql', {});\n    const fromFile = doc && /\\.(graphql|gql)$/i.test(doc.name) ? doc.selectedText || doc.text : '';\n    play = await ferrum.window.showPanel({\n      title: 'GraphQL',\n      html: playgroundPage({ endpoint: saved.endpoint || String(ferrum.settings.get('apiTools.graphqlEndpoint') || ''), query: fromFile || saved.query || '{\\n  __typename\\n}', variables: saved.variables || '{}' }),\n    });\n    play.onDidClose(() => (play = null));\n    play.onMessage(async (m) => {\n      if (m.type !== 'run' && m.type !== 'schema') return;\n      try {\n        let vars = {};\n        if (m.type === 'run' && m.variables && m.variables.trim()) {\n          try { vars = JSON.parse(m.variables); } catch { throw new Error('The variables are not valid JSON.'); }\n        }\n        const body = m.type === 'schema' ? { query: INTROSPECTION } : { query: m.query, variables: vars };\n        const { res, text, ms } = await send(m, body);\n        let shown = text;\n        try {\n          const json = JSON.parse(text);\n          shown = m.type === 'schema' ? schemaOutline(json) : JSON.stringify(json, null, 2);\n        } catch (e) {\n          if (m.type === 'schema' && e.message.startsWith('The server')) shown = e.message;\n        }\n        if (m.type === 'run') await ferrum.storage.set('graphql', { endpoint: m.endpoint, query: String(m.query).slice(0, 20000), variables: String(m.variables).slice(0, 5000) });\n        play.postMessage({ type: 'result', ok: res.ok, status: res.status + ' ' + (res.statusText || ''), ms, text: shown.slice(0, 200000) });\n      } catch (e) {\n        play.postMessage({ type: 'result', ok: false, status: 'Error', ms: 0, text: e.message });\n      }\n    });\n  });\n};\n\nexports.deactivate = function () {};\n"},"README.md":{"encoding":"utf8","content":"# API Tools\n\n## OpenAPI viewer\n\nOpen an OpenAPI 3 or Swagger 2 file (`openapi.yaml`, `swagger.json`...) and run **View the OpenAPI Description**, or run it with a folder open and it finds the file. A panel shows the title, version and servers, then the endpoints grouped by tag with colored method badges, parameters, request bodies and responses, and the schemas. Search filters everything; **Insert \"GET /pets\"** puts an endpoint in your file. **List Endpoints...** is the same as a quick list. YAML is read with a built in reader for the part of YAML that OpenAPI files use (anchors and merge keys are not supported).\n\n## GraphQL playground\n\nType an endpoint, a query, variables and headers, then **Run query**. **Show schema** runs an introspection query and lists the queries, mutations and types. Open a `.graphql` file first and its text becomes the query. The endpoint, query and variables are remembered; **headers (and any token in them) are never saved**.\n\n## WebSocket tester?\n\nNot included: extensions run in a sandbox without WebSocket, so it cannot be done.\n\n## Permissions\n\n**workspace.read**, to find your API description. **network**, to send the GraphQL query to the endpoint you typed. Nothing else is contacted.\n"}}}