diff --git a/src/ApiMethodDocumentation.js b/src/ApiMethodDocumentation.js index 9bdee2d..416338a 100644 --- a/src/ApiMethodDocumentation.js +++ b/src/ApiMethodDocumentation.js @@ -239,7 +239,11 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { * Bindings for the type document. * This is a map of the type name to the binding name. */ - bindings: {type: Array} + bindings: {type: Array}, + /** + * Controls whether the metadata section is opened + */ + metadataOpened: { type: Boolean } }; } @@ -864,17 +868,93 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { method } = this; return html` - ${this._getTitleTemplate()} + ${this._getEnhancedSummaryTemplate()} ${this._deprecatedWarningTemplate()} ${this._getUrlTemplate()} ${this._getTraitsTemplate()} ${hasCustomProperties ? html`` : ''} ${this._getDescriptionTemplate()} - ${this._getRequestTemplate()} - ${this._getReturnsTemplate()} + ${this._getEnhancedRequestTemplate()} + ${this._getEnhancedResponseTemplate()} + ${this._getEnhancedSecurityTemplate()} + ${this._getEnhancedExamplesTemplate()} + ${this._getEnhancedMetadataTemplate()} ${this._getNavigationTemplate()}`; } + /** + * Enhanced summary template with gRPC support and better organization + */ + _getEnhancedSummaryTemplate() { + if (this._titleHidden) { + return ''; + } + + const { method, methodName, noTryIt, compatibility, methodSummary, operationId } = this; + const isGrpc = this.isGrpcOp(method); + const isAsyncApi = this._isAsyncAPI(this.amf); + + return html` +
+
+
+
${methodName}
+ ${this._getMethodBadgeTemplate(method, isGrpc)} +
+ ${noTryIt ? '' : html`
+ Try it +
`} +
+ ${methodSummary ? html`

${methodSummary}

` : ''} + ${operationId && !isAsyncApi ? html`Operation ID: ${operationId}` : ''} +
+ `; + } + + /** + * Gets method badge template for REST/gRPC operations + */ + _getMethodBadgeTemplate(method, isGrpc) { + if (!method) { + return ''; + } + + const httpMethod = this._getValue(method, this.ns.aml.vocabularies.apiContract.method); + + if (isGrpc) { + const streamType = this.getGrpcStreamType(method); + return html` +
+ gRPC + ${streamType !== 'unary' ? html`${this._formatStreamType(streamType)}` : ''} +
+ `; + } + + return html` +
+ ${httpMethod?.toUpperCase()} +
+ `; + } + + /** + * Formats stream type for display + */ + _formatStreamType(streamType) { + const typeMap = { + 'client_streaming': 'Client Stream', + 'server_streaming': 'Server Stream', + 'bidi_streaming': 'Bidirectional Stream', + 'unary': 'Unary' + }; + return typeMap[streamType] || streamType; + } + _getTitleTemplate() { const isAsyncApi = this._isAsyncAPI(this.amf) if (this._titleHidden) { @@ -1108,6 +1188,204 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { >`; } + /** + * Enhanced request template with structured information + */ + _getEnhancedRequestTemplate() { + const { method } = this; + if (!method) { + return ''; + } + + const expects = this._computeExpects(method); + if (!expects) { + return ''; + } + + const isGrpc = this.isGrpcOp(method); + const payloads = this.getPayloads(expects); + const params = this.getParams(method); + const hasParams = Object.values(params).some(paramArray => paramArray.length > 0); + + return html` +
+
Request
+ + ${this._getRequestPayloadTemplate(payloads, isGrpc)} + ${hasParams ? this._getEnhancedParametersTemplate(params) : ''} + ${this._getRequestHeadersTemplate(expects)} + + + ${this._getAsyncSecurityMethodTemplate()} + ${this._getMessagesTemplate()} + ${this._getCodeSnippetsTemplate()} + ${this._getAgentTemplate()} + ${this._callbacksTemplate()} +
+ `; + } + + /** + * Gets request payload template + */ + _getRequestPayloadTemplate(payloads, isGrpc) { + if (!payloads || !Array.isArray(payloads) || payloads.length === 0) { + return html`
No request body
`; + } + + return html` +
+
Body
+ ${payloads.map(payload => this._getPayloadTemplate(payload, isGrpc, 'request'))} +
+ `; + } + + /** + * Gets payload template for request/response + */ + _getPayloadTemplate(payload, isGrpc, type = 'request') { + const mediaType = this.getMediaType(payload); + const schema = this._getValue(payload, this.ns.aml.vocabularies.shapes.schema); + const examples = this.getExamples(payload); + + let schemaName = 'Unknown'; + if (schema && schema.length > 0) { + schemaName = this.getSchemaName(schema[0]) || 'Schema'; + } + + return html` +
+
+ ${mediaType || 'application/json'} + ${isGrpc ? html`${schemaName}` : ''} +
+ + ${schema && schema.length > 0 ? this._getSchemaTemplate(schema[0], isGrpc) : ''} + ${examples.length > 0 ? this._getPayloadExamplesTemplate(examples, type) : ''} +
+ `; + } + + /** + * Gets schema template + */ + _getSchemaTemplate(schema, isGrpc) { + const schemaName = this.getSchemaName(schema); + const description = this._getValue(schema, this.ns.aml.vocabularies.core.description); + + if (isGrpc) { + // Resolve schema if it's a link + const resolvedSchema = this._resolveSchemaLink(schema); + if (!resolvedSchema) { + return html` +
+
${schemaName} Message
+
Schema definition not found
+
+ `; + } + + // For gRPC, show message structure from resolved schema + const properties = this._ensureArray(this._getValue(resolvedSchema, this.ns.w3.shacl.property)); + if (properties && properties.length > 0) { + return html` +
+
${schemaName} Message
+ ${description ? html`
${description}
` : ''} +
+ ${properties.map(prop => this._getGrpcFieldTemplate(prop))} +
+
+ `; + } + + return html` +
+
${schemaName} Message
+ ${description ? html`
${description}
` : ''} +
No fields defined
+
+ `; + } + + // For REST, use existing body document component + return html` + + + `; + } + + /** + * Gets gRPC field template + */ + _getGrpcFieldTemplate(property) { + const name = this._getValue(property, this.ns.w3.shacl.name); + const range = this._getValue(property, this.ns.aml.vocabularies.shapes.range); + const minCount = this._getValue(property, this.ns.w3.shacl.minCount); + const required = minCount && minCount > 0; + + let fieldType = this._getFieldTypeFromRange(range); + + return html` +
+ ${name} + ${fieldType} + ${required ? html`required` : ''} +
+ `; + } + + /** + * Gets field type string from range object + * @param {Object} range AMF Range object + * @returns {string} Field type description + */ + _getFieldTypeFromRange(range) { + if (!range) { + return 'string'; + } + + // Check if it's an array + if (this._hasType(range, 'http://a.ml/vocabularies/shapes#ArrayShape')) { + const items = this._getValue(range, this.ns.aml.vocabularies.shapes.items); + if (items) { + const itemType = this._getFieldTypeFromRange(items); + return `${itemType}[]`; + } + return 'array'; + } + + // Check if it's a scalar + if (this._hasType(range, 'http://a.ml/vocabularies/shapes#ScalarShape')) { + const dataType = this._getValue(range, this.ns.w3.shacl.datatype); + if (dataType) { + const typeUri = dataType['@id'] || dataType; + if (typeUri.includes('#string')) return 'string'; + if (typeUri.includes('#int')) return 'int32'; + if (typeUri.includes('#long')) return 'int64'; + if (typeUri.includes('#float')) return 'float'; + if (typeUri.includes('#double')) return 'double'; + if (typeUri.includes('#boolean')) return 'bool'; + return typeUri.split('#').pop() || 'string'; + } + } + + // Check if it's a node shape (message type) + if (this._hasType(range, 'http://www.w3.org/ns/shacl#NodeShape')) { + const name = this._getValue(range, this.ns.w3.shacl.name) || + this._getValue(range, this.ns.aml.vocabularies.core.name); + return name || 'message'; + } + + return 'string'; + } + _getRequestTemplate() { return html`
${this._getAsyncSecurityMethodTemplate()} @@ -1207,6 +1485,184 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { this.expects = this.message[event.target.selected] } + /** + * Enhanced parameters template with better organization + */ + _getEnhancedParametersTemplate(params) { + const hasAnyParams = Object.values(params).some(paramArray => paramArray.length > 0); + if (!hasAnyParams) { + return ''; + } + + return html` +
+
Parameters
+ ${params.path.length > 0 ? this._getParameterGroupTemplate('Path', params.path) : ''} + ${params.query.length > 0 ? this._getParameterGroupTemplate('Query', params.query) : ''} + ${params.header.length > 0 ? this._getParameterGroupTemplate('Header', params.header) : ''} + ${params.cookie.length > 0 ? this._getParameterGroupTemplate('Cookie', params.cookie) : ''} +
+ `; + } + + /** + * Gets parameter group template + */ + _getParameterGroupTemplate(groupName, parameters) { + return html` +
+
${groupName} Parameters
+
+ ${parameters.map(param => this._getParameterItemTemplate(param))} +
+
+ `; + } + + /** + * Gets parameter item template + */ + _getParameterItemTemplate(param) { + return html` +
+
+ ${param.name} + ${param.type} + ${param.required ? html`required` : ''} +
+ ${param.description ? html`
${param.description}
` : ''} + ${param.defaultValue ? html`
Default: ${param.defaultValue}
` : ''} + ${param.examples.length > 0 ? html`
Examples: ${param.examples.map(ex => html`${ex}`).join(', ')}
` : ''} +
+ `; + } + + /** + * Gets request headers template + */ + _getRequestHeadersTemplate(expects) { + const headers = this._ensureArray(this._getValue(expects, this.ns.aml.vocabularies.apiContract.header)); + if (!headers || headers.length === 0) { + return ''; + } + + return html` +
+
Headers
+ + +
+ `; + } + + /** + * Enhanced response template + */ + _getEnhancedResponseTemplate() { + const { returns, method } = this; + if (!returns || !returns.length || this._isAsyncAPI(this.amf)) { + return ''; + } + + const isGrpc = this.isGrpcOp(method); + + return html` +
+
Response
+ + ${isGrpc ? this._getGrpcResponseTemplate(returns) : this._getRestResponseTemplate(returns)} +
+ `; + } + + /** + * Gets gRPC response template + */ + _getGrpcResponseTemplate(returns) { + const response = returns[0]; // gRPC typically has one response + const payloads = this.getPayloads(response); + + if (!payloads || !Array.isArray(payloads)) { + return ''; + } + + return html` +
+ ${payloads.map(payload => this._getPayloadTemplate(payload, true, 'response'))} +
+ `; + } + + /** + * Gets REST response template + */ + _getRestResponseTemplate(returns) { + return html` +
+ + +
+ `; + } + + /** + * Gets payload examples template + */ + _getPayloadExamplesTemplate(examples, type) { + if (!examples || !Array.isArray(examples) || examples.length === 0) { + return ''; + } + + return html` +
+
Example${examples.length > 1 ? 's' : ''}
+ ${examples.map((example, index) => this._getExampleTemplate(example, index, type))} +
+ `; + } + + /** + * Gets example template + */ + _getExampleTemplate(example, index, type) { + const value = this._getValue(example, this.ns.aml.vocabularies.core.value); + const name = this._getValue(example, this.ns.aml.vocabularies.core.name) || `Example ${index + 1}`; + + if (!value) { + return ''; + } + + const isLongExample = value.length > 200; + const displayValue = isLongExample ? value.substring(0, 200) + '...' : value; + + return html` +
+
+ ${name} +
+
+
${displayValue}
+ ${isLongExample ? html` + + Show full example + + ` : ''} +
+
+ `; + } + _getReturnsTemplate() { const { returns } = this; if (!returns || !returns.length || this._isAsyncAPI(this.amf)) { @@ -1305,42 +1761,293 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { this.httpMethod = event.detail.method; } - isNonHttpProtocol() { - const { protocol } = this; - if (!protocol) { - return false; + /** + * Enhanced security template + */ + _getEnhancedSecurityTemplate() { + const { method } = this; + const securityRequirements = this.getSecurityRequirements(method); + + if (!securityRequirements || securityRequirements.length === 0) { + return ''; } - const lowerCase = protocol.toLowerCase(); - return lowerCase !== 'http' && lowerCase !== 'https'; + + return html` +
+
Security
+
+ ${securityRequirements.map(requirement => this._getSecurityRequirementTemplate(requirement))} +
+
+ `; } - _getNavigationTemplate() { - const { next, previous, noNavigation } = this; - if (!next && !previous || noNavigation) { + /** + * Gets security requirement template + */ + _getSecurityRequirementTemplate(requirement) { + const scheme = this._getValue(requirement, this.ns.aml.vocabularies.security.scheme); + if (!scheme || !scheme.length) { return ''; } - const { compatibility } = this; - return html`
- ${previous ? html`` : ''} - - ${next ? html`` : ''} -
`; + + return html` + + + `; } - _computeMethodParametersUri(method) { - let queryParams = ''; + /** + * Enhanced examples template + */ + _getEnhancedExamplesTemplate() { + const { method } = this; if (!method) { - return queryParams; + return ''; + } + + const isGrpc = this.isGrpcOp(method); + const examples = this._collectAllExamples(method); + const hasExamples = examples.request.length > 0 || examples.response.length > 0; + + // Generate gRPC examples if it's a gRPC API and we don't have existing examples + let grpcExamples = null; + if (isGrpc && !hasExamples) { + grpcExamples = this._generateGrpcExamples(); + } + + if (!hasExamples && !grpcExamples) { + return ''; + } + + return html` +
+
Examples
+ + ${isGrpc && grpcExamples ? this._getGrpcExamplesTemplate(grpcExamples) : ''} + + ${examples.request && Array.isArray(examples.request) && examples.request.length > 0 ? html` +
+
Request Examples
+ ${examples.request.map((example, index) => this._getExampleTemplate(example, index, 'request'))} +
+ ` : ''} + + ${examples.response && Array.isArray(examples.response) && examples.response.length > 0 ? html` +
+
Response Examples
+ ${examples.response.map((example, index) => this._getExampleTemplate(example, index, 'response'))} +
+ ` : ''} +
+ `; + } + + /** + * Collects all examples from request and response + */ + _collectAllExamples(method) { + const examples = { request: [], response: [] }; + + // Collect request examples + const expects = this._computeExpects(method); + if (expects) { + const payloads = this.getPayloads(expects); + if (payloads && Array.isArray(payloads)) { + payloads.forEach(payload => { + examples.request.push(...this.getExamples(payload)); + }); + } + } + + // Collect response examples + const returns = this._computeReturns(method); + if (returns && returns.length > 0) { + returns.forEach(response => { + const payloads = this.getPayloads(response); + if (payloads && Array.isArray(payloads)) { + payloads.forEach(payload => { + examples.response.push(...this.getExamples(payload)); + }); + } + }); + } + + return examples; + } + + /** + * Enhanced metadata template (collapsible) + */ + _getEnhancedMetadataTemplate() { + const { method } = this; + if (!method) { + return ''; + } + + const metadata = this._collectMetadata(method); + const hasMetadata = Object.values(metadata).some(value => + Array.isArray(value) ? value.length > 0 : Boolean(value) + ); + + if (!hasMetadata) { + return ''; + } + + const { compatibility } = this; + const opened = this.metadataOpened || false; + const label = this._computeToggleActionLabel(opened); + const buttonState = this._computeToggleButtonState(opened); + const iconClass = this._computeToggleIconClass(opened); + + return html` +
+
+
Additional Information
+
+ + ${label} + + +
+
+ + ${this._getMetadataContentTemplate(metadata)} + +
+ `; + } + + /** + * Collects metadata from operation + */ + _collectMetadata(method) { + const tags = this._ensureArray(this._getValue(method, this.ns.aml.vocabularies.core.tag)); + const servers = this._ensureArray(this._getValue(method, this.ns.aml.vocabularies.apiContract.server)); + + return { + operationId: this._getValue(method, this.ns.aml.vocabularies.apiContract.operationId), + tags: Array.isArray(tags) ? tags : [], + externalDocs: this._getValue(method, this.ns.aml.vocabularies.core.documentation), + deprecated: this._getValue(method, this.ns.aml.vocabularies.core.deprecated), + servers: Array.isArray(servers) ? servers : [], + callbacks: this._computeCallbacks(method) + }; + } + + /** + * Gets metadata content template + */ + _getMetadataContentTemplate(metadata) { + return html` +
+ ${metadata.operationId ? html` + + ` : ''} + + ${metadata.tags && Array.isArray(metadata.tags) && metadata.tags.length > 0 ? html` + + ` : ''} + + ${metadata.externalDocs ? html` + + ` : ''} + + ${metadata.deprecated ? html` + + ` : ''} + + ${metadata.servers && Array.isArray(metadata.servers) && metadata.servers.length > 0 ? html` + + ` : ''} +
+ `; + } + + /** + * Toggles metadata section + */ + _toggleMetadata() { + this.metadataOpened = !this.metadataOpened; + } + + /** + * Toggles full example display + */ + _toggleFullExample(index, type) { + // Implementation for showing full examples + console.log(`Toggle full example ${index} for ${type}`); + } + + isNonHttpProtocol() { + const { protocol } = this; + if (!protocol) { + return false; + } + const lowerCase = protocol.toLowerCase(); + return lowerCase !== 'http' && lowerCase !== 'https'; + } + + _getNavigationTemplate() { + const { next, previous, noNavigation } = this; + if (!next && !previous || noNavigation) { + return ''; + } + const { compatibility } = this; + return html`
+ ${previous ? html`` : ''} + + ${next ? html`` : ''} +
`; + } + + _computeMethodParametersUri(method) { + let queryParams = ''; + if (!method) { + return queryParams; } const expects = this._computeExpects(method); @@ -1430,6 +2137,266 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { return params; } + // ========== Enhanced gRPC and AMF Helper Methods ========== + + /** + * Detects if an operation is a gRPC operation + * @param {Object} operation AMF Operation model + * @returns {boolean} True if it's a gRPC operation + */ + isGrpcOp(operation) { + if (!operation) { + return false; + } + + // Check method type for gRPC-specific methods + const method = this._getValue(operation, this.ns.aml.vocabularies.apiContract.method); + if (method && ['publish', 'subscribe', 'pubsub'].includes(method.toLowerCase())) { + return true; + } + + // Check request payload media type for application/grpc + const expects = this._computeExpects(operation); + if (expects) { + const payloads = this.getPayloads(expects); + if (payloads && payloads.length > 0) { + const mediaType = this.getMediaType(payloads[0]); + if (mediaType === 'application/grpc') { + return true; + } + } + } + + return false; + } + + /** + * Gets the gRPC stream type for an operation + * @param {Object} operation AMF Operation model + * @returns {string} Stream type: 'unary', 'client_streaming', 'server_streaming', 'bidi_streaming' + */ + getGrpcStreamType(operation) { + if (!operation) { + return 'unary'; + } + + const method = this._getValue(operation, this.ns.aml.vocabularies.apiContract.method); + if (!method) { + return 'unary'; + } + + // Map gRPC methods to stream types + switch (method.toLowerCase()) { + case 'post': + return 'unary'; // Standard unary RPC + case 'publish': + return 'client_streaming'; // Client sends stream + case 'subscribe': + return 'server_streaming'; // Server sends stream + case 'pubsub': + return 'bidi_streaming'; // Bidirectional streaming + default: + return 'unary'; + } + } + + /** + * Extracts payloads from a request or response node + * @param {Object} node AMF Request or Response model + * @returns {Array} Array of payload objects + */ + getPayloads(node) { + if (!node) { + return []; + } + const payloads = this._ensureArray(this._getValue(node, this.ns.aml.vocabularies.apiContract.payload)); + return Array.isArray(payloads) ? payloads : []; + } + + /** + * Gets the media type from a payload + * @param {Object} payload AMF Payload model + * @returns {string|undefined} Media type string + */ + getMediaType(payload) { + if (!payload) { + return undefined; + } + return this._getValue(payload, this.ns.aml.vocabularies.core.mediaType); + } + + /** + * Gets the schema name from a shape + * @param {Object} shape AMF Shape model + * @returns {string|undefined} Schema name + */ + getSchemaName(shape) { + if (!shape) { + return undefined; + } + // Try w3 name first, then core name + return this._getValue(shape, this.ns.w3.shacl.name) || + this._getValue(shape, this.ns.aml.vocabularies.core.name); + } + + /** + * Gets examples from a node (supports both single example and examples array) + * @param {Object} node AMF node with examples + * @returns {Array} Array of example objects + */ + getExamples(node) { + if (!node) { + return []; + } + + // Try examples array first + const examples = this._ensureArray(this._getValue(node, this.ns.aml.vocabularies.core.examples)); + if (examples && Array.isArray(examples) && examples.length > 0) { + return examples; + } + + // Try single example + const example = this._getValue(node, this.ns.aml.vocabularies.core.example); + if (example) { + return [example]; + } + + return []; + } + + /** + * Gets parameters from operation and endpoint (path, query, header, cookie) + * @param {Object} operation AMF Operation model + * @returns {Object} Grouped parameters by type + */ + getParams(operation) { + const params = { + path: [], + query: [], + header: [], + cookie: [] + }; + + if (!operation) { + return params; + } + + // Get parameters from expects (request) + const expects = this._computeExpects(operation); + if (expects) { + // Query parameters + const queryParams = this._ensureArray(this._getValue(expects, this.ns.aml.vocabularies.apiContract.parameter)); + if (queryParams && Array.isArray(queryParams)) { + queryParams.forEach(param => { + const binding = this._getValue(param, this.ns.aml.vocabularies.apiBinding.binding); + const paramType = binding ? binding.toLowerCase() : 'query'; + + if (params[paramType]) { + params[paramType].push(this._processParameter(param)); + } + }); + } + + // Header parameters + const headers = this._ensureArray(this._getValue(expects, this.ns.aml.vocabularies.apiContract.header)); + if (headers && Array.isArray(headers)) { + headers.forEach(header => { + params.header.push(this._processParameter(header)); + }); + } + } + + // Get path parameters from endpoint + if (this.endpoint) { + const pathParams = this._ensureArray(this._getValue(this.endpoint, this.ns.aml.vocabularies.apiContract.parameter)); + if (pathParams && Array.isArray(pathParams)) { + pathParams.forEach(param => { + params.path.push(this._processParameter(param)); + }); + } + } + + return params; + } + + /** + * Processes a parameter to extract relevant information + * @param {Object} param AMF Parameter model + * @returns {Object} Processed parameter info + */ + _processParameter(param) { + if (!param) { + return {}; + } + + const name = this._getValue(param, this.ns.aml.vocabularies.core.name); + const required = this._getValue(param, this.ns.aml.vocabularies.apiContract.required); + const schema = this._getValue(param, this.ns.aml.vocabularies.shapes.schema); + const description = this._getValue(param, this.ns.aml.vocabularies.core.description); + + let type = 'string'; + let defaultValue; + let examples = []; + + if (schema) { + const schemaArray = this._ensureArray(schema); + if (schemaArray.length > 0) { + const schemaObj = schemaArray[0]; + const dataType = this._getValue(schemaObj, this.ns.w3.shacl.datatype); + if (dataType) { + // Extract type from XML Schema URI + type = dataType.split('#').pop() || 'string'; + } + + defaultValue = this._getValue(schemaObj, this.ns.w3.shacl.defaultValue); + examples = this.getExamples(schemaObj); + } + } + + return { + name, + type, + required: Boolean(required), + defaultValue, + description, + examples: examples.map(ex => this._getValue(ex, this.ns.aml.vocabularies.core.value)).filter(Boolean) + }; + } + + /** + * Gets security requirements for operation with fallback to API level + * @param {Object} operation AMF Operation model + * @returns {Array} Array of security requirements + */ + getSecurityRequirements(operation) { + if (!operation) { + return []; + } + + // Try operation-level security first + let security = this._computeSecurity(operation); + + // Fallback to server/API level security + if (!security || security.length === 0) { + security = this._computeSecurity(this.server) || this._computeSecurity(this.amf); + } + + return this._ensureArray(security); + } + + /** + * Compacts a value using AMF key resolution + * @param {Object} node AMF node + * @param {string} key Property key + * @returns {*} Compacted value + */ + compactValue(node, key) { + if (!node || !key) { + return undefined; + } + return this._getValue(node, this._getAmfKey(key)); + } + /** * Returns message value depending on operation node method * Subscribe -> returns @@ -1465,4 +2432,353 @@ export class ApiMethodDocumentation extends AmfHelperMixin(LitElement) { * @param {String} selected * @param {String} type */ + + /** + * Generates gRPC JSON example from payload schema + * @param {Object} payload AMF Payload model + * @returns {Object} Generated example with JSON and grpcurl command + */ + generateGrpcExample(payload) { + if (!payload) { + return { json: '{}', grpcurl: '' }; + } + + const schema = this._getValue(payload, this.ns.aml.vocabularies.shapes.schema); + if (!schema) { + return { json: '{}', grpcurl: '' }; + } + + // Generate JSON example from schema + const jsonExample = this._generateJsonFromSchema(schema); + + // Get service and method name for grpcurl + const serviceName = this._getServiceName(); + const methodName = this._getMethodName(); + const serverUrl = this._getGrpcServerUrl(); + + const grpcurlCommand = `grpcurl -plaintext -d '${JSON.stringify(jsonExample)}' ${serverUrl} ${serviceName}.${methodName}`; + + return { + json: JSON.stringify(jsonExample, null, 2), + grpcurl: grpcurlCommand + }; + } + + /** + * Generates JSON example from AMF schema + * @param {Object} schema AMF Shape model + * @returns {Object} Generated JSON object + */ + _generateJsonFromSchema(schema) { + if (!schema) { + return {}; + } + + // Resolve schema if it's a link + const resolvedSchema = this._resolveSchemaLink(schema); + if (!resolvedSchema) { + return {}; + } + + const properties = this._ensureArray(this._getValue(resolvedSchema, this.ns.w3.shacl.property)); + if (!properties || !Array.isArray(properties)) { + return {}; + } + + const example = {}; + + properties.forEach(property => { + const name = this._getValue(property, this.ns.w3.shacl.name) || + this._getValue(property, this.ns.aml.vocabularies.core.name); + const range = this._getValue(property, this.ns.aml.vocabularies.shapes.range); + + if (name) { + example[name] = this._getExampleValueFromRange(range, name); + } + }); + + return example; + } + + /** + * Gets example value based on data type + * @param {string} dataType Schema data type + * @param {string} fieldName Field name for context + * @returns {any} Example value + */ + _getExampleValueForType(dataType, fieldName) { + if (!dataType) { + return fieldName.toLowerCase().includes('name') ? 'example' : 'value'; + } + + const typeStr = dataType.toString().toLowerCase(); + + if (typeStr.includes('string')) { + if (fieldName.toLowerCase().includes('name')) return 'world'; + if (fieldName.toLowerCase().includes('message')) return 'Hello World!'; + if (fieldName.toLowerCase().includes('email')) return 'user@example.com'; + return 'example'; + } + + if (typeStr.includes('int') || typeStr.includes('number')) { + return 42; + } + + if (typeStr.includes('bool')) { + return true; + } + + if (typeStr.includes('array')) { + return ['item1', 'item2']; + } + + return 'value'; + } + + /** + * Gets gRPC service name from current endpoint + * @returns {string} Service name + */ + _getServiceName() { + if (this.endpoint) { + const serviceName = this._getValue(this.endpoint, this.ns.aml.vocabularies.core.name); + if (serviceName) { + return serviceName; + } + } + return 'greeter'; + } + + /** + * Gets gRPC method name from current method + * @returns {string} Method name + */ + _getMethodName() { + if (this.method) { + const methodName = this._getValue(this.method, this.ns.aml.vocabularies.core.name); + if (methodName) { + return methodName; + } + } + return 'SayHello'; + } + + /** + * Gets gRPC server URL + * @returns {string} Server URL + */ + _getGrpcServerUrl() { + // Try to get server from AMF model + if (this.amf) { + const servers = this._ensureArray(this._getValue(this.amf, this.ns.aml.vocabularies.apiContract.server)); + if (servers && Array.isArray(servers) && servers.length > 0) { + const serverUrl = this._getValue(servers[0], this.ns.aml.vocabularies.core.url); + if (serverUrl) { + return serverUrl; + } + } + } + + // Default gRPC server + return 'localhost:50051'; + } + + /** + * Checks if current API is gRPC + * @returns {boolean} True if gRPC API + */ + isGrpcApi() { + return this.isGrpcOp(this.method); + } + + /** + * Resolves schema link to actual schema definition + * @param {Object} schema AMF Schema that might be a link + * @returns {Object} Resolved schema or original if not a link + */ + _resolveSchemaLink(schema) { + if (!schema) { + return null; + } + + // Check if this schema has a link-target (reference to another schema) + const linkTarget = this._getValue(schema, this.ns.aml.vocabularies.document.linkTarget); + if (linkTarget) { + // Find the actual schema definition in the AMF model + return this._findSchemaById(linkTarget['@id'] || linkTarget); + } + + return schema; + } + + /** + * Finds schema definition by ID in the AMF model + * @param {string} schemaId The ID of the schema to find + * @returns {Object} Schema definition or null if not found + */ + _findSchemaById(schemaId) { + if (!this.amf || !schemaId) { + return null; + } + + // Look through all documents in the AMF model + const documents = Array.isArray(this.amf) ? this.amf : [this.amf]; + + for (const doc of documents) { + // Check declares section + const declares = this._ensureArray(this._getValue(doc, this.ns.aml.vocabularies.document.declares)); + if (declares && Array.isArray(declares)) { + for (const declaration of declares) { + if (declaration['@id'] === schemaId) { + return declaration; + } + } + } + + // Check references section + const references = this._ensureArray(this._getValue(doc, this.ns.aml.vocabularies.document.references)); + if (references && Array.isArray(references)) { + for (const reference of references) { + const found = this._findSchemaById(schemaId); + if (found) return found; + } + } + } + + return null; + } + + /** + * Gets example value from range (handles arrays, scalars, etc.) + * @param {Object} range AMF Range object + * @param {string} fieldName Field name for context + * @returns {any} Example value + */ + _getExampleValueFromRange(range, fieldName) { + if (!range) { + return this._getExampleValueForType(null, fieldName); + } + + // Check if it's an array + if (this._hasType(range, 'http://a.ml/vocabularies/shapes#ArrayShape')) { + const items = this._getValue(range, this.ns.aml.vocabularies.shapes.items); + if (items) { + const itemValue = this._getExampleValueFromRange(items, fieldName); + return [itemValue]; + } + return []; + } + + // Check if it's a scalar + if (this._hasType(range, 'http://a.ml/vocabularies/shapes#ScalarShape')) { + const dataType = this._getValue(range, this.ns.w3.shacl.datatype); + if (dataType) { + return this._getExampleValueForType(dataType['@id'] || dataType, fieldName); + } + } + + // Fallback to default value based on field name + return this._getExampleValueForType(null, fieldName); + } + + /** + * Checks if an object has a specific type + * @param {Object} obj AMF object + * @param {string} type Type URI to check + * @returns {boolean} True if object has the type + */ + _hasType(obj, type) { + if (!obj || !obj['@type']) { + return false; + } + const types = Array.isArray(obj['@type']) ? obj['@type'] : [obj['@type']]; + return types.includes(type); + } + + /** + * Generates gRPC examples for request and response + * @returns {Object} Generated gRPC examples + */ + _generateGrpcExamples() { + const expects = this._computeExpects(this.method); + const returns = this._computeReturns(this.method); + + let requestExample = null; + let responseExample = null; + + // Generate request example + if (expects) { + const requestPayloads = this.getPayloads(expects); + if (requestPayloads && requestPayloads.length > 0) { + requestExample = this.generateGrpcExample(requestPayloads[0]); + } + } + + // Generate response example + if (returns && returns.length > 0) { + const responsePayloads = this.getPayloads(returns[0]); + if (responsePayloads && responsePayloads.length > 0) { + responseExample = this.generateGrpcExample(responsePayloads[0]); + } + } + + return { + request: requestExample, + response: responseExample + }; + } + + /** + * Gets gRPC examples template + * @param {Object} grpcExamples Generated gRPC examples + * @returns {import('lit-html').TemplateResult} Template for gRPC examples + */ + _getGrpcExamplesTemplate(grpcExamples) { + const serviceName = this._getServiceName(); + const methodName = this._getMethodName(); + const serverUrl = this._getGrpcServerUrl(); + + return html` +
+
gRPC Examples
+ + ${grpcExamples.request ? html` +
+
Request JSON (Proto3)
+
+
${grpcExamples.request.json}
+ +
+
+ +
+
grpcurl Command
+
+
${grpcExamples.request.grpcurl}
+ +
+
+ ` : ''} + + ${grpcExamples.response ? html` +
+
Response JSON (Proto3)
+
+
${grpcExamples.response.json}
+ +
+
+ ` : ''} + +
+
Server Information
+
+

Service: ${serviceName}

+

Method: ${methodName}

+

Server: ${serverUrl}

+
+
+
+ `; + } } diff --git a/src/Styles.js b/src/Styles.js index 4805190..d247977 100644 --- a/src/Styles.js +++ b/src/Styles.js @@ -333,4 +333,506 @@ api-security-documentation:last-of-type { .async-method-security{ margin-top: 17px; } + +/* Enhanced API Method Documentation Styles */ + +/* Operation Summary */ +.operation-summary { + margin-bottom: 24px; +} + +.operation-header { + display: flex; + align-items: center; + gap: 12px; + flex-wrap: wrap; +} + +.method-badges { + display: flex; + align-items: center; + gap: 8px; +} + +.method-badge { + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + color: white; + display: inline-block; +} + +/* HTTP Method Colors */ +.method-badge.http-method-get { + background-color: var(--api-method-documentation-method-get-color, #61affe); +} + +.method-badge.http-method-post { + background-color: var(--api-method-documentation-method-post-color, #49cc90); +} + +.method-badge.http-method-put { + background-color: var(--api-method-documentation-method-put-color, #fca130); +} + +.method-badge.http-method-delete { + background-color: var(--api-method-documentation-method-delete-color, #f93e3e); +} + +.method-badge.http-method-patch { + background-color: var(--api-method-documentation-method-patch-color, #50e3c2); +} + +.method-badge.http-method-head { + background-color: var(--api-method-documentation-method-head-color, #9012fe); +} + +.method-badge.http-method-options { + background-color: var(--api-method-documentation-method-options-color, #0d5aa7); +} + +/* gRPC Badge */ +.method-badge.grpc-badge { + background-color: var(--api-method-documentation-grpc-color, #4285f4); +} + +.stream-badge { + padding: 2px 6px; + border-radius: 3px; + font-size: 10px; + font-weight: 500; + background-color: var(--api-method-documentation-stream-badge-bg, #e8f0fe); + color: var(--api-method-documentation-stream-badge-color, #1a73e8); + border: 1px solid var(--api-method-documentation-stream-badge-border, #dadce0); +} + +/* Enhanced Request/Response */ +.no-request-body { + color: var(--api-method-documentation-muted-color, #666); + font-style: italic; + margin: 8px 0; +} + +.request-payload, +.enhanced-parameters, +.request-headers { + margin: 16px 0; +} + +.payload-item { + border: 1px solid var(--api-method-documentation-border-color, #e0e0e0); + border-radius: 4px; + margin: 8px 0; + overflow: hidden; +} + +.payload-header { + background-color: var(--api-method-documentation-payload-header-bg, #f5f5f5); + padding: 8px 12px; + display: flex; + align-items: center; + gap: 8px; + border-bottom: 1px solid var(--api-method-documentation-border-color, #e0e0e0); +} + +.media-type-badge { + padding: 2px 6px; + background-color: var(--api-method-documentation-media-type-bg, #e3f2fd); + color: var(--api-method-documentation-media-type-color, #1976d2); + border-radius: 3px; + font-size: 11px; + font-weight: 500; +} + +.schema-name { + font-weight: 600; + color: var(--api-method-documentation-schema-name-color, #333); +} + +/* gRPC Message Styles */ +.grpc-message { + padding: 12px; +} + +.schema-title { + font-weight: 600; + margin-bottom: 8px; + color: var(--api-method-documentation-schema-title-color, #333); +} + +.schema-description { + color: var(--api-method-documentation-description-color, #666); + margin-bottom: 12px; + font-size: 14px; +} + +.message-fields { + display: flex; + flex-direction: column; + gap: 8px; +} + +.grpc-field { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + background-color: var(--api-method-documentation-field-bg, #fafafa); + border-radius: 3px; +} + +.field-name { + font-weight: 500; + color: var(--api-method-documentation-field-name-color, #333); +} + +.field-type { + color: var(--api-method-documentation-field-type-color, #666); + font-size: 12px; + font-family: var(--arc-font-code-family, monospace); +} + +.required-badge { + padding: 1px 4px; + background-color: var(--api-method-documentation-required-bg, #ff5722); + color: white; + border-radius: 2px; + font-size: 10px; + font-weight: 500; +} + +/* Enhanced Parameters */ +.parameter-group { + margin: 12px 0; +} + +.parameter-group-title { + font-weight: 600; + margin-bottom: 8px; + color: var(--api-method-documentation-param-group-color, #333); +} + +.parameters-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.parameter-item { + border: 1px solid var(--api-method-documentation-border-color, #e0e0e0); + border-radius: 4px; + padding: 8px; +} + +.parameter-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.parameter-name { + font-weight: 500; + color: var(--api-method-documentation-param-name-color, #333); +} + +.parameter-type { + color: var(--api-method-documentation-param-type-color, #666); + font-size: 12px; + font-family: var(--arc-font-code-family, monospace); +} + +.parameter-description { + color: var(--api-method-documentation-description-color, #666); + font-size: 14px; + margin: 4px 0; +} + +.parameter-default, +.parameter-examples { + font-size: 12px; + color: var(--api-method-documentation-muted-color, #666); + margin: 2px 0; +} + +.parameter-default code, +.parameter-examples code { + background-color: var(--api-method-documentation-code-bg, #f5f5f5); + padding: 1px 3px; + border-radius: 2px; + font-family: var(--arc-font-code-family, monospace); +} + +/* Examples */ +.payload-examples, +.enhanced-examples { + margin: 16px 0; +} + +.examples-title { + font-weight: 600; + margin-bottom: 8px; + color: var(--api-method-documentation-examples-title-color, #333); +} + +.example-item { + border: 1px solid var(--api-method-documentation-border-color, #e0e0e0); + border-radius: 4px; + margin: 8px 0; + overflow: hidden; +} + +.example-header { + background-color: var(--api-method-documentation-example-header-bg, #f8f9fa); + padding: 6px 8px; + border-bottom: 1px solid var(--api-method-documentation-border-color, #e0e0e0); +} + +.example-name { + font-weight: 500; + font-size: 12px; + color: var(--api-method-documentation-example-name-color, #333); +} + +.example-content { + position: relative; +} + +.example-content pre { + margin: 0; + padding: 12px; + background-color: var(--api-method-documentation-code-bg, #f8f9fa); + overflow-x: auto; + font-size: 12px; + line-height: 1.4; +} + +.example-content code { + font-family: var(--arc-font-code-family, monospace); + color: var(--api-method-documentation-code-color, #333); +} + +.show-full-example { + position: absolute; + bottom: 8px; + right: 8px; + font-size: 11px; + padding: 4px 8px; +} + +/* Enhanced Security */ +.enhanced-security { + margin: 24px 0; +} + +.security-requirements { + display: flex; + flex-direction: column; + gap: 12px; +} + +/* Enhanced Metadata */ +.enhanced-metadata { + margin: 24px 0; +} + +.metadata-content { + padding: 16px; +} + +.metadata-item { + display: flex; + align-items: flex-start; + gap: 8px; + margin: 8px 0; +} + +.metadata-item.deprecated { + align-items: center; +} + +.metadata-label { + font-weight: 600; + color: var(--api-method-documentation-metadata-label-color, #333); + min-width: 120px; +} + +.metadata-value { + color: var(--api-method-documentation-metadata-value-color, #666); + font-family: var(--arc-font-code-family, monospace); + font-size: 13px; +} + +.metadata-tags { + display: flex; + flex-wrap: wrap; + gap: 4px; +} + +.tag-badge { + padding: 2px 6px; + background-color: var(--api-method-documentation-tag-bg, #e8f5e8); + color: var(--api-method-documentation-tag-color, #2e7d32); + border-radius: 3px; + font-size: 11px; + font-weight: 500; +} + +.deprecated-badge { + padding: 2px 6px; + background-color: var(--api-method-documentation-deprecated-bg, #ffebee); + color: var(--api-method-documentation-deprecated-color, #c62828); + border-radius: 3px; + font-size: 11px; + font-weight: 500; + border: 1px solid var(--api-method-documentation-deprecated-border, #ffcdd2); +} + +.server-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.server-item { + font-family: var(--arc-font-code-family, monospace); + font-size: 12px; + color: var(--api-method-documentation-server-color, #666); + padding: 2px 4px; + background-color: var(--api-method-documentation-server-bg, #f5f5f5); + border-radius: 2px; +} + +/* gRPC Examples Styles */ +.grpc-examples { + margin: 24px 0; +} + +.example-section { + margin: 16px 0; +} + +.example-snippet { + position: relative; + border: 1px solid var(--api-method-documentation-border-color, #e0e0e0); + border-radius: 4px; + overflow: hidden; +} + +.example-snippet pre { + margin: 0; + padding: 16px; + background-color: var(--api-method-documentation-code-bg, #f8f9fa); + overflow-x: auto; + font-family: var(--arc-font-code-family, 'Consolas', 'Monaco', 'Courier New', monospace); + font-size: 13px; + line-height: 1.4; +} + +.example-snippet code { + color: var(--api-method-documentation-code-color, #333); +} + +.grpcurl-command pre { + background-color: var(--api-method-documentation-terminal-bg, #2d3748); +} + +.grpcurl-command code { + color: var(--api-method-documentation-terminal-color, #e2e8f0); +} + +.example-snippet clipboard-copy { + position: absolute; + top: 8px; + right: 8px; + --anypoint-button-background-color: var(--api-method-documentation-copy-button-bg, #ffffff); + --anypoint-button-color: var(--api-method-documentation-copy-button-color, #333); +} + +.server-info { + margin: 20px 0; + padding: 16px; + background-color: var(--api-method-documentation-info-bg, #f0f8ff); + border: 1px solid var(--api-method-documentation-info-border, #b3d9ff); + border-radius: 4px; +} + +.server-details p { + margin: 4px 0; + font-size: 14px; +} + +.server-details strong { + color: var(--api-method-documentation-label-color, #333); +} + +.heading4 { + font-size: 16px; + font-weight: 600; + margin: 8px 0; + color: var(--api-method-documentation-heading4-color, #333); +} + +/* Enhanced gRPC Badge Styles */ +.grpc-badge { + background: linear-gradient(135deg, #4285f4 0%, #34a853 100%) !important; + color: white !important; + box-shadow: 0 2px 4px rgba(66, 133, 244, 0.3); +} + +.stream-badge { + background: linear-gradient(135deg, #e8f0fe 0%, #d2e3fc 100%); + color: #1a73e8; + border: 1px solid #dadce0; + font-weight: 600; + text-transform: capitalize; +} + +/* gRPC Message Field Enhancements */ +.grpc-field { + border-left: 3px solid var(--api-method-documentation-grpc-accent, #4285f4); +} + +.grpc-field .field-type { + background-color: var(--api-method-documentation-type-bg, #f1f3f4); + padding: 2px 6px; + border-radius: 3px; + font-weight: 500; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .operation-header { + flex-direction: column; + align-items: flex-start; + } + + .method-badges { + align-self: flex-start; + } + + .parameter-header { + flex-direction: column; + align-items: flex-start; + gap: 4px; + } + + .metadata-item { + flex-direction: column; + gap: 4px; + } + + .metadata-label { + min-width: auto; + } + + .example-snippet clipboard-copy { + position: relative; + top: auto; + right: auto; + margin: 8px; + } +} `;