Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@typespec/http-client-js"
---

Subclient parameters are now included in accessor methods. When a subclient has different constructor parameters from its parent, the parent client now generates an accessor method (instead of no accessor) that accepts the subclient's unique parameters and uses stored shared parameters to create the subclient.
7 changes: 0 additions & 7 deletions packages/http-client-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,31 @@

- [#9446](https://github.com/microsoft/typespec/pull/9446) Upgrade dependencies


Comment thread
JoshLove-msft marked this conversation as resolved.
## 0.13.0

### Bump dependencies

- [#9202](https://github.com/microsoft/typespec/pull/9202) Update to alloy 0.22
- [#9223](https://github.com/microsoft/typespec/pull/9223) Upgrade dependencies


## 0.12.0

### Bump dependencies

- [#9046](https://github.com/microsoft/typespec/pull/9046) Upgrade dependencies


## 0.11.0

### Bump dependencies

- [#8823](https://github.com/microsoft/typespec/pull/8823) Upgrade dependencies


## 0.10.0

### Bug Fixes

- [#8613](https://github.com/microsoft/typespec/pull/8613) Remove warning when no explicit content type is provided to a multipart part


## 0.9.0

### Features
Expand All @@ -50,7 +45,6 @@

- [#8362](https://github.com/microsoft/typespec/pull/8362) Upgrade alloy to 0.20


## 0.8.0

### Bump dependencies
Expand All @@ -62,7 +56,6 @@

- [#8056](https://github.com/microsoft/typespec/pull/8056) Fix the missing `main` field issue in package.json


## 0.7.0

### Bump dependencies
Expand Down
173 changes: 171 additions & 2 deletions packages/http-client-js/src/components/client.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { For, List, Refkey, refkey } from "@alloy-js/core";
import * as ts from "@alloy-js/typescript";
import { type Typekit } from "@typespec/compiler/typekit";
Comment thread
JoshLove-msft marked this conversation as resolved.
Outdated
import { useTsp } from "@typespec/emitter-framework";
import { ClassMethod } from "@typespec/emitter-framework/typescript";
import * as cl from "@typespec/http-client";
Expand Down Expand Up @@ -45,6 +46,44 @@ export function getClientClassRef(client: cl.Client) {
function getClientContextFieldRef(client: cl.Client) {
return refkey(client.type, "client-context");
}

/**
* Returns a stable refkey for a private stored-parameter field on the parent client.
* These stored fields are used by subclient accessor methods to pass shared parameters.
*/
function getStoredParamFieldRef(parentClient: cl.Client, paramName: string): Refkey {
return refkey(parentClient.type, `stored-${paramName}`);
}

/**
* Returns the set of parameter names that are shared between a parent client and a subclient,
* where "shared" means the parameter has the same name AND the same TypeSpec type in both clients.
* Options parameters are excluded.
*/
function getCompatibleSharedParamNames(
tk: Typekit,
parentClient: cl.Client,
subClient: cl.Client,
): Set<string> {
const parentConstructor = tk.client.getConstructor(parentClient);
const parentParams = tk.operation.getClientSignature(parentClient, parentConstructor);
const parentParamsByName = new Map(parentParams.map((p) => [p.name, p]));

const subClientConstructor = tk.client.getConstructor(subClient);
const subClientParams = tk.operation.getClientSignature(subClient, subClientConstructor);

const shared = new Set<string>();
for (const sp of subClientParams) {
if (sp.name === "options") continue;
const pp = parentParamsByName.get(sp.name);
// Only treat as shared if the TypeSpec type is the same object (identity check)
if (pp && pp.type === sp.type) {
shared.add(sp.name);
}
}
return shared;
}

export function ClientClass(props: ClientClassProps) {
const { $ } = useTsp();
const namePolicy = ts.useTSNamePolicy();
Expand All @@ -54,6 +93,12 @@ export function ClientClass(props: ClientClassProps) {
const clientClassRef = getClientClassRef(props.client);
const subClients = props.client.subClients;
const operations = props.client.operations;

// Subclients with different constructors need accessor methods instead of fields
const diffConstrSubClients = subClients.filter(
(sc) => !$.client.haveSameConstructor(sc, props.client),
);

return (
<ts.ClassDeclaration export name={clientName} refkey={clientClassRef}>
<List hardline>
Expand All @@ -63,6 +108,8 @@ export function ClientClass(props: ClientClassProps) {
refkey={contextMemberRef}
type={contextDeclarationRef}
/>
{/* Private stored-parameter fields used by subclient accessor methods */}
<StoredParamFields client={props.client} diffConstrSubClients={diffConstrSubClients} />
<For each={subClients} hardline semicolon>
{(subClient) => <SubClientClassField client={subClient} />}
</For>
Expand Down Expand Up @@ -91,11 +138,61 @@ export function ClientClass(props: ClientClassProps) {
);
}}
</For>
{/* Accessor methods for subclients with different constructor parameters */}
<For each={diffConstrSubClients} hardline semicolon>
{(subClient) => (
<SubClientAccessorMethod subClient={subClient} parentClient={props.client} />
)}
</For>
</List>
</ts.ClassDeclaration>
);
}

interface StoredParamFieldsProps {
client: cl.Client;
diffConstrSubClients: cl.Client[];
}

/**
* Renders private class fields for each constructor parameter that is shared between the parent
* client and any of its different-constructor subclients. These stored fields are referenced by
* the subclient accessor methods.
*/
function StoredParamFields(props: StoredParamFieldsProps) {
const { $ } = useTsp();
const parentParams = buildClientParameters(props.client, refkey());

// Collect unique shared param names/types across all different-constructor subclients
const storedParamMap = new Map<string, ts.ParameterDescriptor>();
for (const subClient of props.diffConstrSubClients) {
const sharedNames = getCompatibleSharedParamNames($, props.client, subClient);
for (const name of sharedNames) {
if (!storedParamMap.has(name)) {
const parentParam = parentParams.find((pp) => String(pp.name) === name);
if (parentParam) {
storedParamMap.set(name, parentParam);
}
}
}
}

const storedParams = [...storedParamMap.values()];

return (
<For each={storedParams} hardline semicolon>
{(param) => (
<ts.ClassField
name={String(param.name)}
jsPrivate
refkey={getStoredParamFieldRef(props.client, String(param.name))}
type={param.type}
/>
)}
</For>
);
}

interface SubClientClassFieldProps {
client: cl.Client;
}
Expand All @@ -107,8 +204,7 @@ function getSubClientClassFieldRef(client: cl.Client) {
function SubClientClassField(props: SubClientClassFieldProps) {
const { $ } = useTsp();
const parent = props.client.parent;
// If sub client has different parameters than client, don't add it as a subclass field
// Todo: We need to detect the extra parameters and make this field a factory for the subclient
// Subclients with different constructor parameters are exposed as accessor methods instead
if (parent && !$.client.haveSameConstructor(props.client, parent)) {
return null;
}
Expand All @@ -129,15 +225,42 @@ function ClientConstructor(props: ClientConstructorProps) {
const subClients = props.client.subClients.filter((sc) =>
$.client.haveSameConstructor(sc, props.client),
);
const diffConstrSubClients = props.client.subClients.filter(
(sc) => !$.client.haveSameConstructor(sc, props.client),
);
const clientContextFieldRef = getClientContextFieldRef(props.client);
const clientContextFactoryRef = getClientContextFactoryRef(props.client);
const constructorParameters = buildClientParameters(props.client, refkey());
const args = Object.values(constructorParameters).map((p) => p.refkey);

// Compute the stored params needed for different-constructor subclient accessor methods
const storedParamNames = new Set<string>();
for (const subClient of diffConstrSubClients) {
const sharedNames = getCompatibleSharedParamNames($, props.client, subClient);
for (const name of sharedNames) {
storedParamNames.add(name);
}
}
const storedParamAssignments = [...storedParamNames].map((name) => {
const parentParam = constructorParameters.find((p) => String(p.name) === name)!;
return {
name,
storedRef: getStoredParamFieldRef(props.client, name),
paramRef: parentParam.refkey,
};
});

return (
<ts.ClassMethod name="constructor" parameters={constructorParameters}>
{clientContextFieldRef} ={" "}
<ts.FunctionCallExpression target={clientContextFactoryRef} args={args} />;<br />
<For each={storedParamAssignments} joiner=";" hardline>
{(assignment) => (
<>
{assignment.storedRef} = {assignment.paramRef};
</>
)}
</For>
<For each={subClients} joiner=";" hardline>
{(subClient) => {
const subClientFieldRef = getSubClientClassFieldRef(subClient);
Expand All @@ -153,6 +276,52 @@ function ClientConstructor(props: ClientConstructorProps) {
);
}

interface SubClientAccessorMethodProps {
subClient: cl.Client;
parentClient: cl.Client;
}

/**
* Renders an accessor method for a subclient whose constructor parameters differ from the parent.
* The method takes the subclient's extra parameters (those not shared with the parent) and uses
* the parent's stored shared parameters to construct and return the subclient.
*/
function SubClientAccessorMethod(props: SubClientAccessorMethodProps) {
const { $ } = useTsp();
const namePolicy = ts.useTSNamePolicy();

const accessorSuffixRefkey = refkey();
const subClientParams = buildClientParameters(props.subClient, accessorSuffixRefkey);

// Shared params: same name AND same TypeSpec type in both parent and subclient
const sharedParamNames = getCompatibleSharedParamNames($, props.parentClient, props.subClient);

// Extra params (only in subclient or different type) and the options param → accessor method parameters
const accessorMethodParams = subClientParams.filter((p) => !sharedParamNames.has(String(p.name)));

// Build the argument list for `new SubClient(...)` following the subclient's constructor order
const subClientConstructorArgs: Refkey[] = subClientParams.flatMap((p) => {
const paramName = String(p.name);
if (paramName === "options") {
const optionsParam = accessorMethodParams.find((ap) => String(ap.name) === "options");
return optionsParam?.refkey ? optionsParam.refkey : [];
} else if (sharedParamNames.has(paramName)) {
return [getStoredParamFieldRef(props.parentClient, paramName)];
} else {
const methodParam = accessorMethodParams.find((ap) => String(ap.name) === paramName);
return methodParam?.refkey ? methodParam.refkey : [];
}
});

const methodName = namePolicy.getName($.client.getName(props.subClient), "class");

return (
<ts.ClassMethod name={methodName} parameters={accessorMethodParams}>
return <NewClientExpression client={props.subClient} args={subClientConstructorArgs} />;
</ts.ClassMethod>
);
}

function calculateSubClientArgs(subClient: cl.Client, parentParams: ts.ParameterDescriptor[]) {
const subClientParams = buildClientParameters(subClient, refkey()).map((p) => p.name);
return parentParams
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,22 @@ namespace Sub {

The client signature should include a positional parameter for credential of type KeyCredential. A basic auth token is a key credential that gets put into the Authorization header/

The subclient is not a child of the TestClient because they have different parameter.
The subclient is accessible from the TestClient via an accessor method since they have different constructor parameters. The accessor method takes only the subclient's unique parameters.

```ts src/testClient.ts class TestClient
export class TestClient {
#context: TestClientContext;
#endpoint: string;
constructor(endpoint: string, credential: BasicCredential, options?: TestClientOptions) {
this.#context = createTestClientContext(endpoint, credential, options);
this.#endpoint = endpoint;
}
async valid(options?: ValidOptions) {
return valid(this.#context, options);
}
subClient(options?: SubClientOptions) {
return new SubClient(this.#endpoint, options);
}
}
```

Expand Down
Loading