function selectGrpcSource(source) { grpcSource = source; document.querySelectorAll('.grpc-source-btn').forEach(function(btn) { btn.classList.toggle('active', btn.dataset.source === source); }); document.getElementById('grpc-src-proto').hidden = source !== 'proto'; document.getElementById('grpc-src-reflection').hidden = source !== 'reflection'; document.getElementById('grpc-src-manual').hidden = source !== 'manual'; var detail = document.getElementById('grpc-method-detail'); if (detail) detail.hidden = true; selectedRpcMethod = null; } function extractBlocks(text, keyword) { var blocks = []; var re = new RegExp(keyword + '\\s+(\\w+)\\s*\\{', 'g'); var match; while ((match = re.exec(text)) !== null) { var name = match[1]; var start = match.index + match[0].length; var depth = 1; var i = start; while (i < text.length && depth > 0) { if (text[i] === '{') depth++; else if (text[i] === '}') depth--; i++; } blocks.push({ name: name, body: text.slice(start, i - 1), start: match.index }); } return blocks; } function stripProtoComments(text) { text = text.replace(/\/\*[\s\S]*?\*\//g, ''); text = text.replace(/\/\/[^\n]*/g, ''); return text; } function parseProto(text) { text = stripProtoComments(text); var pkg = ''; var pkgMatch = text.match(/\bpackage\s+([\w.]+)\s*;/); if (pkgMatch) pkg = pkgMatch[1]; var messages = {}; extractBlocks(text, 'message').forEach(function(blk) { var fields = []; var fieldRe = /\b(repeated\s+)?([\w.]+)\s+(\w+)\s*=\s*\d+/g; var fm; while ((fm = fieldRe.exec(blk.body)) !== null) { if (['option', 'reserved', 'extensions', 'oneof'].indexOf(fm[2]) !== -1) continue; fields.push({ repeated: !!fm[1], type: fm[2], name: fm[3] }); } messages[blk.name] = fields; }); var streamingCount = 0; var services = []; extractBlocks(text, 'service').forEach(function(svc) { var methods = []; var rpcRe = /\brpc\s+(\w+)\s*\(\s*(stream\s+)?([\w.]+)\s*\)\s*returns\s*\(\s*(stream\s+)?([\w.]+)\s*\)/g; var rm; while ((rm = rpcRe.exec(svc.body)) !== null) { var streamIn = !!rm[2]; var streamOut = !!rm[4]; if (streamIn || streamOut) { streamingCount += 1; } else { var reqType = rm[3].split('.').pop(); var resType = rm[5].split('.').pop(); methods.push({ name: rm[1], request: reqType, response: resType }); } } if (methods.length > 0) { services.push({ name: svc.name, methods: methods }); } }); return { package: pkg, services: services, messages: messages, streamingCount: streamingCount }; } function renderServiceResults(parsed, ids) { protoParsed = parsed; selectedRpcMethod = null; var notice = document.getElementById(ids.notice); var countEl = document.getElementById(ids.count); if (notice) { notice.hidden = parsed.streamingCount <= 0; if (countEl && parsed.streamingCount > 0) countEl.textContent = parsed.streamingCount; } var totalMethods = parsed.services.reduce(function(n, s) { return n + s.methods.length; }, 0); var summaryEl = document.getElementById(ids.summary); if (summaryEl) { var servicesLabel = window.tPlural('wizard.grpc.services_label', parsed.services.length, { count: parsed.services.length, }); var methodsLabel = window.tPlural('wizard.grpc.methods_label', totalMethods, { count: totalMethods, }); summaryEl.textContent = tfKey('wizard.grpc.services_found', { services_label: servicesLabel, methods_label: methodsLabel, }); } var listEl = document.getElementById(ids.list); if (!listEl) return; listEl.innerHTML = ''; if (parsed.services.length === 0) { var emptyDiv = document.createElement('div'); emptyDiv.className = 'proto-empty'; emptyDiv.textContent = tKey('wizard.grpc.no_methods'); listEl.appendChild(emptyDiv); } else { var svcTmpl = document.getElementById('tmpl-proto-service-group'); var methodTmpl = document.getElementById('tmpl-proto-method-card'); parsed.services.forEach(function(svc) { var svcNode = svcTmpl.content.cloneNode(true); svcNode.querySelector('.proto-service-name').textContent = svc.name; var grid = svcNode.querySelector('.proto-method-grid'); svc.methods.forEach(function(m) { var mNode = methodTmpl.content.cloneNode(true); var btn = mNode.querySelector('.proto-method-card'); btn.dataset.service = svc.name; btn.dataset.method = m.name; btn.dataset.req = m.request; btn.dataset.res = m.response; mNode.querySelector('.proto-method-name').textContent = m.name; mNode.querySelector('.proto-req-type').textContent = m.request; mNode.querySelector('.proto-res-type').textContent = m.response; grid.appendChild(mNode); }); listEl.appendChild(svcNode); }); } document.getElementById(ids.view).hidden = false; document.getElementById('grpc-method-detail').hidden = true; } function renderParsedProto(parsed) { var notice = document.getElementById('proto-streaming-text'); if (notice && parsed && parsed.streamingCount) { notice.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, { count: parsed.streamingCount, }); } renderServiceResults(parsed, { notice: 'proto-streaming-notice', count: 'proto-streaming-count', summary: 'proto-services-summary', list: 'proto-services-list', view: 'proto-parsed-view', }); } function renderReflectionResult(parsed) { var reflectText = document.querySelector('#reflect-streaming-notice .info-callout-text'); if (reflectText && parsed && parsed.streamingCount) { reflectText.textContent = window.tPlural('wizard.step3.grpc.streaming_hidden', parsed.streamingCount, { count: parsed.streamingCount, }); } renderServiceResults(parsed, { notice: 'reflect-streaming-notice', count: 'reflect-streaming-count', summary: 'reflect-services-summary', list: 'reflect-services-list', view: 'reflect-parsed-view', }); } function selectRpcMethod(btn) { document.querySelectorAll('.proto-method-card').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); var svcName = btn.dataset.service; var methodName = btn.dataset.method; var reqType = btn.dataset.req; var resType = btn.dataset.res; selectedRpcMethod = { service: svcName, method: methodName, request: reqType, response: resType }; var pkg = protoParsed && protoParsed.package ? protoParsed.package + '.' : ''; var grpcPath = '/' + pkg + svcName + '/' + methodName; var pathEl = document.getElementById('grpc-path-value'); if (pathEl) pathEl.textContent = grpcPath; var sub = document.getElementById('grpc-method-subtitle'); if (sub) sub.textContent = 'rpc ' + methodName + ' (' + reqType + ') returns (' + resType + ')'; var reqTypeEl = document.getElementById('grpc-req-type'); var resTypeEl = document.getElementById('grpc-res-type'); if (reqTypeEl) reqTypeEl.textContent = reqType; if (resTypeEl) resTypeEl.textContent = resType; var sigGrid = document.getElementById('grpc-detail-sig'); if (sigGrid) sigGrid.hidden = false; renderFieldList('grpc-req-fields', reqType); renderFieldList('grpc-res-fields', resType); document.getElementById('grpc-method-detail').hidden = false; } function renderFieldList(elId, typeName) { var el = document.getElementById(elId); if (!el) return; var fields = protoParsed && protoParsed.messages[typeName]; el.innerHTML = ''; if (!fields || fields.length === 0) { var emptyEl = document.createElement('div'); emptyEl.className = 'proto-field-empty'; emptyEl.textContent = ''; emptyEl.appendChild(document.createTextNode(tKey('wizard.grpc.fields_unresolved'))); emptyEl.appendChild(document.createElement('br')); var hint = document.createElement('span'); hint.style.color = 'var(--text-muted)'; hint.style.fontSize = '11px'; hint.textContent = tKey('wizard.grpc.fields_unresolved_body'); emptyEl.appendChild(hint); el.appendChild(emptyEl); return; } var fieldTmpl = document.getElementById('tmpl-proto-field-row'); fields.forEach(function(f) { var node = fieldTmpl.content.cloneNode(true); node.querySelector('.proto-field-name').textContent = f.name; node.querySelector('.proto-field-type').textContent = (f.repeated ? 'repeated ' : '') + f.type; el.appendChild(node); }); } function handleProtoFile(event) { var file = event.target.files[0]; if (!file) return; var reader = new FileReader(); reader.onload = function(e) { wizardProtoUpload = { fileName: file.name, content: e.target.result }; showProtoFileInfo(file.name, file.size); var parsed = parseProto(e.target.result); renderParsedProto(parsed); }; reader.readAsText(file); } function handleProtoDrop(event) { event.preventDefault(); var dropzone = document.getElementById('proto-dropzone'); if (dropzone) dropzone.classList.remove('drag-over'); var file = event.dataTransfer.files[0]; if (!file || !file.name.endsWith('.proto')) return; var reader = new FileReader(); reader.onload = function(e) { wizardProtoUpload = { fileName: file.name, content: e.target.result }; showProtoFileInfo(file.name, file.size); renderParsedProto(parseProto(e.target.result)); }; reader.readAsText(file); } function showProtoFileInfo(name, size) { var dropzone = document.getElementById('proto-dropzone'); var info = document.getElementById('proto-file-info'); var nameEl = document.getElementById('proto-file-name'); var sizeEl = document.getElementById('proto-file-size'); if (dropzone) dropzone.hidden = true; if (info) info.hidden = false; if (nameEl) nameEl.textContent = name; if (sizeEl) sizeEl.textContent = (size / 1024).toFixed(1) + ' KB'; document.getElementById('proto-paste-area').hidden = true; document.getElementById('proto-paste-btn').textContent = 'Paste content'; } function resetProto() { var dropzone = document.getElementById('proto-dropzone'); var info = document.getElementById('proto-file-info'); var parsedView = document.getElementById('proto-parsed-view'); if (dropzone) dropzone.hidden = false; if (info) info.hidden = true; if (parsedView) parsedView.hidden = true; var fileInput = document.getElementById('proto-file-input'); if (fileInput) fileInput.value = ''; protoParsed = null; selectedRpcMethod = null; wizardProtoUpload = null; } function toggleProtoPaste() { var area = document.getElementById('proto-paste-area'); var btn = document.getElementById('proto-paste-btn'); if (!area) return; var open = !area.hidden; area.hidden = open; if (btn) btn.textContent = open ? 'Paste content' : 'Cancel paste'; if (!open) { var ta = document.getElementById('proto-paste-input'); if (ta) setTimeout(function() { ta.focus(); }, 50); } } function cancelProtoPaste() { var area = document.getElementById('proto-paste-area'); var btn = document.getElementById('proto-paste-btn'); if (area) area.hidden = true; if (btn) btn.textContent = 'Paste content'; } function parseProtoPasted() { var ta = document.getElementById('proto-paste-input'); if (!ta || !ta.value.trim()) return; var parsed = parseProto(ta.value); wizardProtoUpload = { fileName: 'pasted.proto', content: ta.value }; document.getElementById('proto-paste-area').hidden = true; var dropzone = document.getElementById('proto-dropzone'); if (dropzone) dropzone.hidden = true; var info = document.getElementById('proto-file-info'); var nameEl = document.getElementById('proto-file-name'); var sizeEl = document.getElementById('proto-file-size'); if (info) info.hidden = false; if (nameEl) nameEl.textContent = 'pasted.proto'; if (sizeEl) sizeEl.textContent = (ta.value.length / 1024).toFixed(1) + ' KB'; renderParsedProto(parsed); } var REFLECTION_MOCK = { package: 'acme.v1', streamingCount: 2, services: [ { name: 'ContactService', methods: [ { name: 'GetContact', request: 'GetContactRequest', response: 'Contact' }, { name: 'CreateContact', request: 'CreateContactRequest', response: 'Contact' }, { name: 'UpdateContact', request: 'UpdateContactRequest', response: 'Contact' }, { name: 'DeleteContact', request: 'DeleteContactRequest', response: 'DeleteContactResponse' }, ]}, { name: 'CompanyService', methods: [ { name: 'GetCompany', request: 'GetCompanyRequest', response: 'Company' }, { name: 'ListCompanies', request: 'ListCompaniesRequest', response: 'ListCompaniesResponse' }, ]}, ], messages: { 'GetContactRequest': [{ type: 'string', name: 'id' }], Contact: [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }, { type: 'string', name: 'created_at' }], 'CreateContactRequest': [{ type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }, { type: 'string', name: 'company_id' }], 'UpdateContactRequest': [{ type: 'string', name: 'id' }, { type: 'string', name: 'first_name' }, { type: 'string', name: 'last_name' }, { type: 'string', name: 'email' }], 'DeleteContactRequest': [{ type: 'string', name: 'id' }], 'DeleteContactResponse': [{ type: 'bool', name: 'success' }], 'GetCompanyRequest': [{ type: 'string', name: 'id' }], Company: [{ type: 'string', name: 'id' }, { type: 'string', name: 'name' }, { type: 'string', name: 'domain' }, { type: 'int32', name: 'employee_count' }], 'ListCompaniesRequest': [{ type: 'int32', name: 'page' }, { type: 'int32', name: 'page_size' }, { type: 'string', name: 'query' }], 'ListCompaniesResponse': [{ type: 'Company', name: 'companies', repeated: true }, { type: 'int32', name: 'total' }], }, }; function startReflection() { goToStep(5); showWizardLiveStatus( tKey('wizard.grpc.discovery_title'), tKey('wizard.grpc.discovery_body') ); } function grpcManualRouteInput(val) { var detail = document.getElementById('grpc-method-detail'); var sigGrid = document.getElementById('grpc-detail-sig'); if (!val || !val.startsWith('/') || val.indexOf('/', 1) === -1) { if (detail) detail.hidden = true; return; } var parts = val.replace(/^\//, '').split('/'); var servicePart = parts[0] || ''; var methodName = parts[1] || ''; var serviceName = servicePart.split('.').pop(); var pathEl = document.getElementById('grpc-path-value'); if (pathEl) pathEl.textContent = val; var sub = document.getElementById('grpc-method-subtitle'); if (sub) sub.textContent = serviceName + '/' + methodName; if (sigGrid) sigGrid.hidden = true; grpcManualTypeInput(); if (detail) detail.hidden = false; } function grpcManualTypeInput() { var reqInput = document.getElementById('grpc-manual-req-type'); var resInput = document.getElementById('grpc-manual-res-type'); var reqTypeEl = document.getElementById('grpc-req-type'); var resTypeEl = document.getElementById('grpc-res-type'); if (reqTypeEl) reqTypeEl.textContent = (reqInput && reqInput.value.trim()) || '—'; if (resTypeEl) resTypeEl.textContent = (resInput && resInput.value.trim()) || '—'; }