import type { Children } from "@alloy-js/core"; import { code, For, List, NamePolicyContext, useNamePolicy } from "@alloy-js/core"; import * as ts from "@alloy-js/typescript"; import { useTransformNamePolicy } from "@typespec/emitter-framework"; import type { HttpOperation, HttpProperty } from "@typespec/http"; import { getDefaultValue, hasDefaultValue } from "../utils/parameters.jsx"; import { JsonTransform } from "./transforms/json/json-transform.jsx"; export interface HttpRequestParametersExpressionProps { httpOperation: HttpOperation; optionsParameter: Children; parameters?: HttpProperty[]; children?: Children; } export function HttpRequestParametersExpression(props: HttpRequestParametersExpressionProps) { const parameters: Children[] = []; const transformNamer = useTransformNamePolicy(); if (props.children || (Array.isArray(props.children) && props.children.length)) { parameters.push(<>{props.children},); } if (!props.parameters && parameters.length) { return {parameters}; } else if (!props.parameters) { return ; } const optionsParamRef = props.optionsParameter ?? "options"; const members = ( {(httpProperty) => { const defaultValue = getDefaultValue(httpProperty); const propertyExpression = buildMemberChainExpression( props.httpOperation, httpProperty, optionsParamRef, ); const transportParamName = transformNamer.getTransportName(httpProperty.property); if (defaultValue) { const paramValue = ( <> {propertyExpression.fullExpression} {` ?? ${defaultValue}`} ); return ( n, for: () => (n) => n }}> ); } if (propertyExpression.isNullish) { return code` ...(${propertyExpression.fullExpression} && {${()}}) `; } else { return ( ); } }} ); parameters.push(members); return {parameters}; } interface MemberChainExpression { propertyName: string; isNullish: boolean; fullExpression: Children; leadingExpression?: Children; } /** * Builds the member chain expression for potentially nested HTTP parameters. * This logic will be simplified and made more robust once a couple of changes land in Alloy. * - Support refkeys on interface members * - Support for conditional chaining on ts.MemberChainExpression * - Eventually we'll have an API like: * @example * ```tsx * * ``` * @param httpOperation - The HttpOperation containing parameters. * @param httpProperty - The current HttpProperty to build an access path for. * @param optionsParamRef - Reference to the user's main options parameter. * @returns The full nested expression, leading expression, nullish flag, and final property name. */ function buildMemberChainExpression( httpOperation: HttpOperation, httpProperty: HttpProperty, optionsParamRef?: Children, ): MemberChainExpression { const segments: Children[] = []; let isNullish = httpProperty.property.optional === true; const namePolicy = useNamePolicy(); const propertyApplicationName = namePolicy.getName(httpProperty.property.name, "property"); // If the property is at the top level, return early if (httpProperty.path.length === 1) { if (httpProperty.property.optional || hasDefaultValue(httpProperty)) { return { propertyName: propertyApplicationName, isNullish, fullExpression: code`${optionsParamRef}?.${propertyApplicationName}`, leadingExpression: code`${optionsParamRef}`, }; } return { propertyName: propertyApplicationName, isNullish, fullExpression: propertyApplicationName, }; } // Build access segments for nested properties. let currentPath = httpProperty.path.slice(0, -1); while (currentPath.length > 0) { const parentProperty = findHttpPropertyByPath(httpOperation, currentPath); if (!parentProperty) { break; } // Use optional chaining if the parent is optional. const joiner = parentProperty.property.optional ? "?." : "."; if (parentProperty.property.optional) { isNullish = true; } const parentName = namePolicy.getName(parentProperty.property.name, "property"); // Unshift the joiner first segments.unshift(joiner); // Use direct property access if possible. if (isValidIdentifier(parentName)) { segments.unshift(code`${parentName}`); } else { // For non-valid identifiers, use bracket notation. segments.unshift(`[${JSON.stringify(parentName)}]`); } // Remove the last element to move one level up. currentPath = currentPath.slice(0, -1); } const fullExpression = ( <> {propertyApplicationName} ); const leadingExpression = ; return { propertyName: propertyApplicationName, isNullish, fullExpression, leadingExpression, }; } function isValidIdentifier(name: string): boolean { return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name); } function findHttpPropertyByPath( httpOperation: HttpOperation, path: (string | number)[], ): HttpProperty | undefined { for (const property of httpOperation.parameters.properties) { // Check if the property path matches the given path if (JSON.stringify(property.path) === JSON.stringify(path)) { return property; } } return undefined; }