Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates extension decorator to support value kinds and adds setExtension API to json-schema emitter #3558

Merged
merged 11 commits into from
Aug 5, 2024

Conversation

chrisradek
Copy link
Member

@chrisradek chrisradek commented Jun 11, 2024

Related to #3336

Summary

This PR introduces 2 changes:

  1. adds a new setExtension API on the @typespec/json-schema emitter to make it easier to author custom extension decorators.
  2. updates the @extension decorator to support taking values to obviate the need for the Json<Data> model.

Example usage of writing a custom extension @added decorator:

export function $added(context, target, value) {
  setExtension(context.program, target, "x-added", value);
}

Example usage of passing in value literals to @extension decorator:

@extension("x-current", Json<["foo"]>)
@extension("x-new", #["foo"])
model Foo {}

Background

The @typespec/json-schema package exports an @extension decorator that allows customers to specify additional properties to be present as part of their emitted JSON schema.

For example, we can add a property - converted to a schema - that indicates when a field was added to the model:

@jsonSchema
model Pet {
  @extension("x-added", "2024-01-01")
  name: string;
}

The decorator also supports emitting the property's value as raw code by wrapping the parameter in the Json<Data> template:

@jsonSchema
model Pet {
  @extension("x-added", Json<"2024-01-01">)
  name: string;
}

Comparison

Below is a diff between the two ways of calling the decorator for clarity:

$schema: https://json-schema.org/draft/2020-12/schema
$id: Pet.yaml
type: object
properties:
  name:
    type: string
-   x-added:
-     type: string
-     const: 2024-01-01
+   x-added: 2024-01-01
required:
  - name

Pain Points

Currently, @extension only supports raw values by wrapping them with the TypeSpec.JsonSchema.Json template. Since this type isn't available in JS code, this forces authors to manually create the wrapped type themselves, e.g.:

/**
 * @param {import("@typespec/compiler").DecoratorContext} context
 * @param {import("@typespec/compiler").Type} target
 * @param {import("@typespec/compiler").StringLiteral} value
 */
export function $added(context, target, value) {
  $extension(context, target, "x-added", {
    kind: "Model",
    name: "Json",
    namespace: { name: "JsonSchema" },
    properties: createRekeyableMap([["value", { type: value }]]),
  });
}

Update @extension to accept values directly and expose setExtension API

This PR exposes a new setExtension API that accepts types and values as input, instead of just Type. This also means ExtensionRecord.value has been loosened:

export interface ExtensionRecord {
  key: string;
- value: Type;
+ value: Type | unknown;
}

In addition, the @extension decorator is updated to support accepting values directly:

- extern dec extension(target: unknown, key: valueof string, value: unknown);
+ extern dec extension(target: unknown, key: valueof string, value: unknown | (valueof unknown));

While this change introduces breaking changes to the @extensions decorator and getExtensions function, it also allows the @extension decorator to support value kinds as input - a feature language feature introduced in 0.57.0.

This would not cause additional breaking changes (beyond getExtensions) for users that already use the TypeSpec.JsonSchema.Json template to pass in raw values. However, there is 1 scenario that would now fail:

  1. Scalar literals/null would now be treated as values instead of types.
    (e.g. @extension("x-bool-literal", true) would need to be updated to @extension("x-bool-literal", typeof true) to preserve current behavior)

Alternatives

Solution - only expose setExtension API

This PR exposes a new setExtension API that accepts types and values as input, instead of just Type. This also means ExtensionRecord.value has been loosened:

export interface ExtensionRecord {
  key: string;
- value: Type;
+ value: Type | any;
}

This does introduce a breaking change in the getExtensions API since ExtensionRecord.value is no longer guaranteed to be Type. However, it is likely this API is not used much if at all outside of this emitter.

Create new setExtensionValue/getExtensionValues functions and alternative @extension decorator

This change has the benefit of not requiring any breaking changes. However, we would now have 2 different but very similar APIs for settings/getting extensions that may lead to confusion, as well as 2 very similar extensions to do essentially the same thing.

@azure-sdk
Copy link
Collaborator

azure-sdk commented Jun 11, 2024

All changed packages have been documented.

  • @typespec/json-schema
Show changes

@typespec/json-schema - breaking ✏️

Updates @extension decorator to support TypeSpec values in addition to types.,> ,> In previous versions of the json-schema emitter, the @extension decorator only accepted types as the value. These are emitted as JSON schemas. In order to add extensions as raw values, types had to be wrapped in the Json<> template when being passed to the @extension decorator.,> ,> This change allows setting TypeSpec values (introduced in TypeSpec 0.57.0) directly instead.,> ,> The following example demonstrates using values directly:,> ,> tsp,> @extension("x-example", #{ foo: "bar" }),> model Foo {},> ,> ,> This change results in scalars being treated as values instead of types. This will result in the @extension decorator emitting raw values for scalar types instead of JSON schema. To preserve the previous behavior, use typeof when passing in a scalar value.,> ,> The following example demonstrates how to pass a scalar value that emits a JSON schema:,> ,> tsp,> @extension("x-example", "foo"),> model Foo {},> ,> ,> To preserve this same behavior, the above example can be updated to the following:,> ,> tsp,> @extension("x-example", typeof "foo"),> model Foo {},>

@azure-sdk
Copy link
Collaborator

You can try these changes at https://cadlplayground.z22.web.core.windows.net/prs/3558/

Check the website changes at https://tspwebsitepr.z22.web.core.windows.net/prs/3558/

packages/json-schema/src/index.ts Outdated Show resolved Hide resolved
packages/json-schema/src/index.ts Outdated Show resolved Hide resolved
packages/json-schema/test/extension.test.ts Outdated Show resolved Hide resolved
@chrisradek chrisradek force-pushed the json-schema-extension-values branch from 50fbb9a to be60574 Compare July 11, 2024 21:44
@chrisradek chrisradek changed the title Adds setExtension API to json-schema emitter Updates extension decorator to support value kinds and adds setExtension API to json-schema emitter Jul 11, 2024
@chrisradek chrisradek added the breaking-change A change that might cause specs or code to break label Jul 11, 2024
@chrisradek chrisradek marked this pull request as ready for review July 11, 2024 21:52
@chrisradek chrisradek linked an issue Jul 18, 2024 that may be closed by this pull request
3 tasks
@azure-sdk
Copy link
Collaborator

You can try these changes here

🛝 Playground 🌐 Website 📚 Next docs

@chrisradek chrisradek added this pull request to the merge queue Aug 5, 2024
Merged via the queue into microsoft:main with commit 72d4351 Aug 5, 2024
22 checks passed
@chrisradek chrisradek deleted the json-schema-extension-values branch August 5, 2024 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change A change that might cause specs or code to break emitter:json-schema
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Expose a TS API for adding extension fields to a schema in a custom decorator
4 participants