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;
}