Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion api/spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
"devDependencies": {
"@fetch-mock/vitest": "0.2.18",
"@typespec/prettier-plugin-typespec": "1.14.0",
"@typespec/prettier-plugin-typespec": "1.16.0",
"@vitest/coverage-v8": "4.1.12",
"prettier": "3.9.6",
"vite": "8.1.6",
Expand Down
15 changes: 8 additions & 7 deletions api/spec/packages/aip/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@
"@openmeter/typespec-typescript": "workspace:*",
"@redocly/cli": "2.40.0",
"@types/node": "26.1.1",
"@typespec/compiler": "1.14.0",
"@typespec/http": "1.14.0",
"@typespec/json-schema": "1.14.0",
"@typespec/openapi": "1.14.0",
"@typespec/openapi3": "1.14.0",
"@typespec/prettier-plugin-typespec": "1.14.0",
"@typespec/rest": "0.84.0",
"@typespec/compiler": "1.16.0",
"@typespec/http": "1.16.0",
"@typespec/json-schema": "1.16.0",
"@typespec/openapi": "1.16.0",
"@typespec/openapi3": "1.16.0",
"@typespec/prettier-plugin-typespec": "1.16.0",
"@typespec/rest": "0.86.0",
"@typespec/versioning": "0.86.0",
"prettier": "3.9.6",
"yaml": "2.9.0"
},
Expand Down
7 changes: 3 additions & 4 deletions api/spec/packages/aip/src/shared/resource.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,9 @@ model ResourceReference<T extends {}> {
id: ULID;
}

alias ExpandableResourceReference<T extends {}> = CustomExpandableResourceReference<
T,
Shared.ResourceReference<T>
>;
alias ExpandableResourceReference<
T extends {}
> = CustomExpandableResourceReference<T, Shared.ResourceReference<T>>;

@doc("{name} or reference.", T)
@friendlyName("{name}OrReference", T)
Expand Down
52 changes: 52 additions & 0 deletions api/spec/packages/aip/test/compiler-compatibility.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict'
import { it } from 'node:test'
import { getFriendlyName, serializeValueAsJson } from '@typespec/compiler'
import { createTestHost, createTestRunner } from '@typespec/compiler/testing'

it('preserves distinct names for visibility-projected nested models', async () => {
const host = await createTestHost()
const runner = await createTestRunner(host)
await runner.compile(`
@friendlyName("ApiChild")
model Child {
@visibility(Lifecycle.Read) id: string;
@visibility(Lifecycle.Create) value: string;
}
model Parent { child: Child; }
model Request is FilterVisibility<
Parent, #{ all: #[Lifecycle.Create] }, "Create{name}"
>;
`)

const models = runner.program.checker.getGlobalNamespaceType().models
const child = models.get('Child')
const requestChild = models.get('Request').properties.get('child').type

// The request clone must not inherit the response model's schema identity.
assert.equal(getFriendlyName(runner.program, child), 'ApiChild')
assert.equal(getFriendlyName(runner.program, requestChild), 'CreateChild')
assert.deepEqual([...requestChild.properties.keys()], ['value'])
})

it('preserves discriminated-union object defaults during JSON serialization', async () => {
const host = await createTestHost()
const runner = await createTestRunner(host)
await runner.compile(`
enum Kind { subscription: "subscription", anchored: "anchored" }
model Subscription { type: Kind.subscription; }
model Anchored { type: Kind.anchored; interval: string; }
@discriminated(#{ discriminatorPropertyName: "type", envelope: "none" })
union Alignment { subscription: Subscription, anchored: Anchored }
const defaultAlignment: Subscription = #{ type: Kind.subscription };
model Settings { alignment?: Alignment = defaultAlignment; }
`)

const property = runner.program.checker
.getGlobalNamespaceType()
.models.get('Settings')
.properties.get('alignment')
assert.deepEqual(
serializeValueAsJson(runner.program, property.defaultValue, property),
{ type: 'subscription' },
)
})
52 changes: 52 additions & 0 deletions api/spec/packages/aip/test/openapi-output-path.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict'
import { fileURLToPath } from 'node:url'
import { describe, it } from 'node:test'
import { createTester } from '@typespec/compiler/testing'

const tester = createTester(fileURLToPath(new URL('..', import.meta.url)), {
libraries: ['@typespec/http', '@typespec/openapi3', '@typespec/versioning'],
})

describe('OpenAPI output paths', () => {
for (const { version, filename } of [
{ version: '../../escape', filename: '.._.._escape.json' },
{ version: '..\\..\\escape', filename: '.._.._escape.json' },
{ version: '/tmp/escape', filename: '_tmp_escape.json' },
{ version: '2026-10-22', filename: '2026-10-22.json' },
]) {
it(`keeps version ${JSON.stringify(version)} inside the output directory`, async () => {
// Spec-derived names must not control directories, while benign versions
// keep their existing filenames and OpenAPI metadata.
const { outputs } = await tester.emit('@typespec/openapi3', {
'output-file': '{version}.json',
}).compile(`
import "@typespec/http";
import "@typespec/versioning";
using TypeSpec.Http;
using TypeSpec.Versioning;
@service
@versioned(Versions)
namespace Service;
enum Versions { v1: ${JSON.stringify(version)} }
@get op read(): string;
`)

assert.deepEqual(Object.keys(outputs), [filename])
assert.equal(JSON.parse(outputs[filename]).info.version, version)
})
}

it('keeps spec-derived service names inside the output directory', async () => {
const { outputs } = await tester.emit('@typespec/openapi3', {
'output-file': '{service-name}.json',
}).compile(`
import "@typespec/http";
using TypeSpec.Http;
@service
namespace \`../../escape\`;
@get op read(): string;
`)

assert.deepEqual(Object.keys(outputs), ['.._.._escape.json'])
})
})
18 changes: 9 additions & 9 deletions api/spec/packages/legacy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@
}
},
"devDependencies": {
"@azure-tools/typespec-client-generator-core": "0.70.0",
"@azure-tools/typespec-client-generator-core": "0.72.0",
"@types/node": "26.1.1",
"@typespec/compiler": "1.14.0",
"@typespec/http": "1.14.0",
"@typespec/compiler": "1.16.0",
"@typespec/http": "1.16.0",
"@typespec/http-client-python": "0.35.0",
"@typespec/openapi": "1.14.0",
"@typespec/openapi3": "1.14.0",
"@typespec/prettier-plugin-typespec": "1.14.0",
"@typespec/rest": "0.84.0",
"@typespec/streams": "0.84.0",
"@typespec/versioning": "0.84.0"
"@typespec/openapi": "1.16.0",
"@typespec/openapi3": "1.16.0",
"@typespec/prettier-plugin-typespec": "1.16.0",
"@typespec/rest": "0.86.0",
"@typespec/streams": "0.86.0",
"@typespec/versioning": "0.86.0"
},
"private": true,
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48"
Expand Down
8 changes: 5 additions & 3 deletions api/spec/packages/legacy/src/notification/invoice.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ namespace OpenMeter;
* Payload for notification event with `invoice.*` type.
*/
@friendlyName("NotificationEventInvoicePayload")
model NotificationEventInvoicePayload<T extends
| NotificationEventType.invoiceCreated
| NotificationEventType.invoiceUpdated> {
model NotificationEventInvoicePayload<
T extends
| NotificationEventType.invoiceCreated
| NotificationEventType.invoiceUpdated
> {
/**
* A unique identifier for the notification event the payload belongs to.
*/
Expand Down
6 changes: 3 additions & 3 deletions api/spec/packages/typespec-go/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
"dependencies": {
"@alloy-js/core": "0.24.1",
"@alloy-js/go": "0.3.1",
"@typespec/compiler": "1.14.0",
"@typespec/compiler": "1.16.0",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"@typespec/emitter-framework": "0.19.0",
"@typespec/http": "1.14.0",
"@typespec/openapi": "1.14.0"
"@typespec/http": "1.16.0",
"@typespec/openapi": "1.16.0"
},
"devDependencies": {
"@alloy-js/cli": "0.24.0",
Expand Down
6 changes: 3 additions & 3 deletions api/spec/packages/typespec-typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
"dependencies": {
"@alloy-js/core": "0.24.1",
"@alloy-js/typescript": "0.24.0",
"@typespec/compiler": "1.14.0",
"@typespec/compiler": "1.16.0",
"@typespec/emitter-framework": "0.19.0",
"@typespec/http": "1.14.0",
"@typespec/http": "1.16.0",
"@typespec/http-client": "0.16.0",
"@typespec/openapi": "1.14.0",
"@typespec/openapi": "1.16.0",
"typescript": "7.0.2"
},
"devDependencies": {
Expand Down
10,338 changes: 0 additions & 10,338 deletions api/spec/patches/@typespec__compiler.patch

This file was deleted.

117 changes: 117 additions & 0 deletions api/spec/patches/@typespec__compiler@1.16.0.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
diff --git a/dist/src/lib/decorators.d.ts b/dist/src/lib/decorators.d.ts
index d47aede90d9a522abdeb5dfd363493a7e8ea22d9..578a05f61a1dcd418ce3d39187309ee40a7f35e1 100644
--- a/dist/src/lib/decorators.d.ts
+++ b/dist/src/lib/decorators.d.ts
@@ -198,6 +198,8 @@ export declare function getAllTags(program: Program, target: Namespace | Interfa
declare const getFriendlyName: (program: Program, type: Type) => string | undefined;
export declare const $friendlyName: FriendlyNameDecorator;
export { getFriendlyName };
+export declare function clearFriendlyName(program: Program, target: Type): void;
+export declare const setFriendlyName: (program: Program, type: Type, value: string) => void;
/**
* `@key` - mark a model property as the key to identify instances of that type
*
diff --git a/dist/src/lib/decorators.js b/dist/src/lib/decorators.js
index 807bd3fabd06766ab8b1faff980da2307dff8f55..40c45d5441c31d934bf3f7a6d8c5363bace74c9a 100644
--- a/dist/src/lib/decorators.js
+++ b/dist/src/lib/decorators.js
@@ -720,7 +720,8 @@ export function getAllTags(program, target) {
return tags.size > 0 ? Array.from(tags).reverse() : undefined;
}
// -- @friendlyName decorator ---------------------
-const [getFriendlyName, setFriendlyName] = useStateMap(createStateSymbol("friendlyNames"));
+const friendlyNamesSymbol = createStateSymbol("friendlyNames");
+const [getFriendlyName, setFriendlyName] = useStateMap(friendlyNamesSymbol);
export const $friendlyName = (context, target, friendlyName, sourceObject) => {
// workaround for current lack of functionality in compiler
// https://github.com/microsoft/typespec/issues/2717
@@ -742,7 +743,10 @@ export const $friendlyName = (context, target, friendlyName, sourceObject) => {
}
setFriendlyName(context.program, target, friendlyName);
};
-export { getFriendlyName };
+export { getFriendlyName, setFriendlyName };
+export function clearFriendlyName(program, target) {
+ program.stateMap(friendlyNamesSymbol).delete(target);
+}
/**
* `@key` - mark a model property as the key to identify instances of that type
*
diff --git a/dist/src/lib/examples.js b/dist/src/lib/examples.js
index df3a1aeacd0fa9f50f01f2247b62d601d9163991..7e980b323afb97ad02bf1886c1566a76cd5f0059 100644
--- a/dist/src/lib/examples.js
+++ b/dist/src/lib/examples.js
@@ -107,6 +107,11 @@ function serializeObjectValueAsJson(program, value, type, handlers, diagnosticTa
obj[name] = serializeValueAsJson(program, propValue.value, definition,
/* encodeAs: */ undefined, handlers, propValue.node);
}
+ else if (type.kind === "Union") {
+ // If the type is still a union (meaning resolveUnions failed to resolve it to a specific variant),
+ // we should still serialize all the properties from the value
+ obj[propValue.name] = serializeValueAsJson(program, propValue.value, program.checker.anyType);
+ }
}
return obj;
}
diff --git a/dist/src/lib/visibility.js b/dist/src/lib/visibility.js
index 3b5a795b2c060ae803462be8d6c1e3ab574f4e08..8bd40cd7c3fdc258ef1a9089c6207417c5060509 100644
--- a/dist/src/lib/visibility.js
+++ b/dist/src/lib/visibility.js
@@ -9,6 +9,7 @@ import { mutateSubgraph, MutatorFlow, setAlwaysMutate } from "../experimental/mu
import { $ } from "../typekit/index.js";
import { useStateMap } from "../utils/index.js";
import { mutate } from "../utils/misc.js";
+import { $friendlyName, setFriendlyName } from "./decorators.js";
import { isKey } from "./key.js";
import { createStateSymbol, filterModelPropertiesInPlace, replaceTemplatedStringFromProperties, } from "./utils.js";
export const $withDefaultKeyVisibility = (context, entity, visibility) => {
@@ -347,7 +348,12 @@ function createVisibilityFilterMutator(filter, options = {}) {
modified ||= variant.type !== member.type;
}
}
- rename(clone, options.nameTemplate);
+ if (options.nameTemplate) {
+ const beforeLen = clone.decorators.length;
+ clone.decorators = clone.decorators.filter((d) => d.decorator !== $friendlyName);
+ modified ||= clone.decorators.length !== beforeLen;
+ }
+ rename(program, clone, options.nameTemplate);
return modified ? clone : union;
},
},
@@ -388,7 +394,12 @@ function createVisibilityFilterMutator(filter, options = {}) {
(options.decoratorName && isTypeSpecDecorator(d, options.decoratorName))));
modified ||= clone.decorators.length !== model.decorators.length;
}
- rename(clone, options.nameTemplate);
+ if (options.nameTemplate) {
+ const beforeLen = clone.decorators.length;
+ clone.decorators = clone.decorators.filter((d) => d.decorator !== $friendlyName);
+ modified ||= clone.decorators.length !== beforeLen;
+ }
+ rename(program, clone, options.nameTemplate);
return modified ? clone : model;
},
},
@@ -427,13 +438,19 @@ function createVisibilityFilterMutator(filter, options = {}) {
return self;
}
/**
- * Internal helper to rename a type in place.
+ * Internal helper to rename a type in place and register the rendered template
+ * as the clone's friendly name. The clone inherits the original's @friendlyName
+ * state (finishType re-applies it from the copied decorators), which would make
+ * the clone collide with the original's schema name; overriding it with the
+ * rendered nameTemplate keeps emitted schema names plain (e.g. "UpdateAddress")
+ * instead of falling back to namespace-qualified type names.
*/
-function rename(type, nameTemplate) {
+function rename(program, type, nameTemplate) {
if (!nameTemplate || !type.name)
return;
const renamed = replaceTemplatedStringFromProperties(nameTemplate, type);
type.name = renamed;
+ setFriendlyName(program, type, renamed);
}
function isVisibilitySubject(t) {
return (t.kind === "Model" ||
Loading
Loading