Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: internal
packages:
- "@typespec/http-specs"
- "@typespec/rest"
---

Add suppressions for deprecated behavior
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
# Change versionKind to one of: internal, fix, dependencies, feature, deprecation, breaking
changeKind: deprecation
packages:
- "@typespec/http"
---

Deprecate use of `@patch(#{implicitOptionality: true})`. Use the explicit `MergePatch<T>` for accurate json merge patch representation
1 change: 1 addition & 0 deletions packages/http-specs/specs/type/model/visibility/main.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ op headModel(@bodyRoot input: VisibilityModel): OkResponse;
@put
op putModel(@body input: VisibilityModel): void;

#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy test"
@scenario
@scenarioDoc("""
Generate abd send put model with write/update properties.
Expand Down
7 changes: 3 additions & 4 deletions packages/http/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,10 @@ Specify the HTTP verb for the target operation to be `PATCH`.
@patch op update(pet: Pet): void;
```

###### Using MergePatch template for proper merge-patch semantics

```typespec
// Disable implicit optionality, making the body of the PATCH operation use the
// optionality as defined in the `Pet` model.
@patch(#{ implicitOptionality: false })
op update(pet: Pet): void;
@patch op update(@body pet: MergePatchUpdate<Pet>): void;
```

#### `@path`
Expand Down
7 changes: 2 additions & 5 deletions packages/http/generated-defs/TypeSpec.Http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,9 @@ export type PostDecorator = (
* ```typespec
* @patch op update(pet: Pet): void;
* ```
* @example
* @example Using MergePatch template for proper merge-patch semantics
* ```typespec
* // Disable implicit optionality, making the body of the PATCH operation use the
* // optionality as defined in the `Pet` model.
* @patch(#{ implicitOptionality: false })
* op update(pet: Pet): void;
* @patch op update(...MergePatchUpdate<Pet>): void;
* ```
*/
export type PatchDecorator = (
Expand Down
8 changes: 3 additions & 5 deletions packages/http/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ model PatchOptions {
/**
* If set to `false`, disables the implicit transform that makes the body of a
* PATCH operation deeply optional.
* @deprecated Use MergePatch templates instead.
*/
implicitOptionality?: boolean;
}
Expand All @@ -281,12 +282,9 @@ model PatchOptions {
* @patch op update(pet: Pet): void;
* ```
*
* @example
* @example Using MergePatch template for proper merge-patch semantics
* ```typespec
* // Disable implicit optionality, making the body of the PATCH operation use the
* // optionality as defined in the `Pet` model.
* @patch(#{ implicitOptionality: false })
* op update(pet: Pet): void;
* @patch op update(@body pet: MergePatchUpdate<Pet>): void;
* ```
*/
extern dec patch(target: Operation, options?: valueof PatchOptions);
Expand Down
10 changes: 9 additions & 1 deletion packages/http/src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,15 @@ export const $patch: PatchDecorator = (
) => {
_patch(context, entity);

if (options) setPatchOptions(context.program, entity, options);
if (options) {
if (options.implicitOptionality === true) {
reportDiagnostic(context.program, {
code: "deprecated-implicit-optionality",
target: entity,
});
}
setPatchOptions(context.program, entity, options);
}
};

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/http/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,10 @@ export const $lib = createTypeSpecLibrary({
default: paramMessage`The 'contents' property of the file model must be a scalar type that extends 'string' or 'bytes'. Found '${"type"}'.`,
},
},
"patch-implicit-optional": {
"deprecated-implicit-optionality": {
severity: "warning",
messages: {
default: `Patch operation stopped applying an implicit optional transform to the body in 1.0.0. Use @patch(#{implicitOptionality: true}) to restore the old behavior.`,
default: `The implicitOptionality option is deprecated. Use MergePatch templates to define the body of a PATCH operation instead.`,
},
},
"merge-patch-contains-null": {
Expand Down
26 changes: 1 addition & 25 deletions packages/http/src/parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import {
Operation,
Program,
} from "@typespec/compiler";
import {
getOperationVerb,
getPatchOptions,
getPathOptions,
getQueryOptions,
} from "./decorators.js";
import { isMergePatchBody } from "./experimental/merge-patch/internal.js";
import { createDiagnostic } from "./lib.js";
import { getOperationVerb, getPathOptions, getQueryOptions } from "./decorators.js";
import { resolveRequestVisibility } from "./metadata.js";
import { HttpPayloadDisposition, resolveHttpPayload } from "./payload.js";
import {
Expand Down Expand Up @@ -123,23 +116,6 @@ function getOperationParametersForVerb(
},
}),
);
const implicitOptionality = getPatchOptions(program, operation)?.implicitOptionality;
// TODO: remove in 6month after 1.0.0. (November 2025)
if (
verb === "patch" &&
resolvedBody &&
implicitOptionality === undefined &&
!isMergePatchBody(program, resolvedBody?.type) &&
!resolvedBody.contentTypes.includes("application/merge-patch+json") // Above statement doesn't detect Spread merge patch
) {
diagnostics.add(
createDiagnostic({
code: "patch-implicit-optional",
target: operation,
}),
);
}

for (const item of metadata) {
switch (item.kind) {
case "contentType":
Expand Down
20 changes: 20 additions & 0 deletions packages/http/test/http-decorators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@ describe("http: decorators", () => {
message: `Argument of type '"/test"' is not assignable to parameter of type 'valueof TypeSpec.Http.PatchOptions'`,
});
});

it(`@patch emits deprecation warning when implicitOptionality: true`, async () => {
const diagnostics = await Tester.diagnose(`
@patch(#{ implicitOptionality: true }) op test(): string;
`);

expectDiagnostics(diagnostics, {
code: "@typespec/http/deprecated-implicit-optionality",
message:
"The implicitOptionality option is deprecated. Use MergePatch templates to define the body of a PATCH operation instead.",
});
});

it(`@patch does not emit deprecation warning when implicitOptionality: false`, async () => {
const diagnostics = await Tester.diagnose(`
@patch(#{ implicitOptionality: false }) op test(): string;
`);

expectDiagnosticEmpty(diagnostics);
});
});

describe("@header", () => {
Expand Down
1 change: 0 additions & 1 deletion packages/http/test/merge-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ describe("http operation support", () => {
name: string;
description?: string;
}
#suppress "@typespec/http/patch-implicit-optional" "For test only ignore correct merge patch"
@patch op update(@header("Content-type") contentType: "application/json", ...MergePatchUpdate<Foo>): void;`);
expectDiagnostics(diag, {
code: "@typespec/http/merge-patch-content-type",
Expand Down
11 changes: 10 additions & 1 deletion packages/openapi3/test/metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
@visibility(Lifecycle.Read, Lifecycle.Update, Lifecycle.Create) ruc?: string;
}
@parameterVisibility(Lifecycle.Create, Lifecycle.Update)
#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@route("/") @patch(#{implicitOptionality: true}) op createOrUpdate(...M): M;
`);

Expand Down Expand Up @@ -176,7 +177,8 @@ worksFor(supportedVersions, ({ openApiFor }) => {
person: Person;
relationship: string;
}
@route("/") @patch(#{implicitOptionality: true}) op update(...Person): Person;
@route("/") #suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) op update(...Person): Person;
`);

const response = res.paths["/"].patch.responses["200"].content["application/json"].schema;
Expand Down Expand Up @@ -217,6 +219,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
weight: float64;
}
@post op create(...Widget): void;
#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) op update(...Widget): void;
`);

Expand Down Expand Up @@ -358,6 +361,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
@get get(...M): M;
@post create(...M): M;
@put createOrUpdate(...M): M;
#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) update(...M): M;
@delete delete(...M): void;
}
Expand All @@ -367,6 +371,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
@get get(...D): D;
@post create(...D): D;
@put createOrUpdate(...D): D;
#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) update(...D): D;
@delete delete(...D): void;
}
Expand All @@ -376,6 +381,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
@get op get(id: string): R;
@post op create(...R): R;
@put op createOrUpdate(...R): R;
#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) op update(...R): R;
@delete op delete(...D): void;
}
Expand All @@ -384,6 +390,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
@get op get(id: string): U;
@post op create(...U): U;
@put op createOrUpdate(...U): U;
#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) op update(...U): U;
@delete op delete(...U): void;
}
Expand Down Expand Up @@ -1087,6 +1094,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
id: uuid;
}

#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) op test(...Bar): Bar;
`);

Expand All @@ -1102,6 +1110,7 @@ worksFor(supportedVersions, ({ openApiFor }) => {
id: string;
}

#suppress "@typespec/http/deprecated-implicit-optionality" "testing legacy behavior"
@patch(#{implicitOptionality: true}) op test(bar: Bar): void;

model Foo {
Expand Down
5 changes: 5 additions & 0 deletions packages/rest/lib/resource.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ interface ResourceCreateOrUpdate<Resource extends {}, Error> {
* @template Resource The resource model to create or update.
* @template Error The error response.
*/
#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior"
@autoRoute
@doc("Creates or update an instance of the resource.")
@createsOrUpdatesResource(Resource)
Expand Down Expand Up @@ -185,6 +186,7 @@ interface ResourceUpdate<Resource extends {}, Error> {
* @template Resource The resource model to update.
* @template Error The error response.
*/
#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior"
@autoRoute
@doc("Updates an existing instance of the resource.")
@updatesResource(Resource)
Expand Down Expand Up @@ -335,6 +337,7 @@ interface SingletonResourceUpdate<Singleton extends {}, Resource extends {}, Err
* @template Singleton The singleton resource model.
* @template Resource The resource model.
*/
#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior"
@autoRoute
@doc("Updates the singleton resource.")
@segmentOf(Singleton)
Expand Down Expand Up @@ -395,6 +398,7 @@ interface ExtensionResourceCreateOrUpdate<Extension extends {}, Resource extends
* @template Extension The extension resource model.
* @template Resource The resource model.
*/
#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior"
@autoRoute
@doc("Creates or update an instance of the extension resource.")
@createsOrUpdatesResource(Extension)
Expand Down Expand Up @@ -443,6 +447,7 @@ interface ExtensionResourceUpdate<Extension extends {}, Resource extends {}, Err
* @template Extension The extension resource model.
* @template Resource The resource model.
*/
#suppress "@typespec/http/deprecated-implicit-optionality" "for legacy behavior"
@autoRoute
@doc("Updates an existing instance of the extension resource.")
@updatesResource(Extension)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,11 +298,10 @@ Specify the HTTP verb for the target operation to be `PATCH`.
@patch op update(pet: Pet): void;
```

##### Using MergePatch template for proper merge-patch semantics

```typespec
// Disable implicit optionality, making the body of the PATCH operation use the
// optionality as defined in the `Pet` model.
@patch(#{ implicitOptionality: false })
op update(pet: Pet): void;
@patch op update(@body pet: MergePatchUpdate<Pet>): void;
```

### `@path` {#@TypeSpec.Http.path}
Expand Down
Loading