Skip to content

Commit f8b7a79

Browse files
Bundle things outside JSON Schema namespace by default (microsoft#2085)
I think it's somewhat unexpected that you might get schemas for types outside of a JSON Schema namespace merely because they are referenced. With this PR, by default we will not emit schemas for such types, and instead bundle them into the referencing schema. As a result, you will only get schemas for things you have explicitly annotated with `@jsonSchema`, either on the declaration or on its containing namespace. For example: ``` @jsonschema model Pet { toy: Toy } model Toy { } ``` will emit a single schema - `Pet.json`, with a `$defs` with Toy. In order to restore the old behavior, you can pass `emitAllRefs`. This is technically a breaking change, but I don't expect this is a common scenario. It's worth thinking about the behavior here when you are importing a library of interesting types that you actually WANT to emit as json schema. There are a few options: * Pass the `emitAllRefs` option. * Model-is the things you want into your JSON Schema namespace. * Use augment decorators on the library's namespace. * Ask the library to add the necessary JSON Schema metadata. The latter three options seem like actually good options since it allows you to provide the namespace/base URI of the schemas, which is considered a best practice. In order to implement this change, a new `TypeEmitter` member is required - `writeOutput` - which lets you customize how you write assets to disk. For this PR, it is used to skip emitting source files that we don't actually need on disk. Another scenario this gets used for is when an emitter composes another emitter and needs to write that emitters output when its own output is needed. An example is a JSON-RPC emitter that has an internal JSON Schema emitter and needs to write JSON Schema to disk. Additionally, `meta` properties were added to declarations and source files to allow for arbitrary metadata to be added. This is far more convenient than establishing maps to maintain this metadata yourself in your emitter, although at the cost of type safety. Fixes microsoft#2022 --------- Co-authored-by: Timothee Guerin <tiguerin@microsoft.com>
1 parent 4515877 commit f8b7a79

File tree

11 files changed

+205
-36
lines changed

11 files changed

+205
-36
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/compiler",
5+
"comment": "Emitter Framework: TypeEmitter can now implement `writeOutput` to customize how to write source files to disk.",
6+
"type": "none"
7+
},
8+
{
9+
"packageName": "@typespec/compiler",
10+
"comment": "Emitter Framework: Source Files and Declarations have a new property `meta` which can store arbitrary metadata about those entities.",
11+
"type": "none"
12+
}
13+
],
14+
"packageName": "@typespec/compiler"
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@typespec/json-schema",
5+
"comment": "By default, types that are not marked with @jsonSchema or are within a namespace with @jsonSchema are bundled into the schemas that reference them. Set the `emitAllRefs` option to true to get the previous behavior of emitting all types referenced as JSON Schema.",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@typespec/json-schema"
10+
}

packages/compiler/src/emitter-framework/asset-emitter.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
compilerAssert,
33
EmitContext,
4-
emitFile,
54
IntrinsicType,
65
isTemplateDeclaration,
76
joinPaths,
@@ -107,6 +106,7 @@ export function createAssetEmitter<T, TOptions extends object>(
107106
let programContext: ContextState | null = null;
108107
let incomingReferenceContext: Record<string, string> | null = null;
109108
const interner = createInterner();
109+
110110
const assetEmitter: AssetEmitter<T, TOptions> = {
111111
getContext() {
112112
return {
@@ -172,6 +172,7 @@ export function createAssetEmitter<T, TOptions extends object>(
172172
globalScope: undefined as any,
173173
path: joinPaths(basePath, path),
174174
imports: new Map(),
175+
meta: {},
175176
};
176177
sourceFile.globalScope = this.createScope(sourceFile, "");
177178
sourceFiles.push(sourceFile);
@@ -292,13 +293,7 @@ export function createAssetEmitter<T, TOptions extends object>(
292293
},
293294

294295
async writeOutput() {
295-
for (const file of sourceFiles) {
296-
const outputFile = typeEmitter.sourceFile(file);
297-
await emitFile(program, {
298-
path: outputFile.path,
299-
content: outputFile.contents,
300-
});
301-
}
296+
return typeEmitter.writeOutput(sourceFiles);
302297
},
303298

304299
emitType(type) {
@@ -425,6 +420,10 @@ export function createAssetEmitter<T, TOptions extends object>(
425420
emitTupleLiteralValues(tuple) {
426421
return invokeTypeEmitter("tupleLiteralValues", tuple);
427422
},
423+
424+
emitSourceFile(sourceFile) {
425+
return typeEmitter.sourceFile(sourceFile);
426+
},
428427
};
429428

430429
const typeEmitter = new TypeEmitterClass(assetEmitter);
@@ -445,6 +444,7 @@ export function createAssetEmitter<T, TOptions extends object>(
445444
| "declarationName"
446445
| "reference"
447446
| "emitValue"
447+
| "writeOutput"
448448
| EndingWith<keyof TypeEmitter<T, TOptions>, "Context">
449449
>
450450
>(method: TMethod, ...args: Parameters<TypeEmitter<T, TOptions>[TMethod]>): EmitEntity<T> {

packages/compiler/src/emitter-framework/type-emitter.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
BooleanLiteral,
33
compilerAssert,
4+
emitFile,
45
Enum,
56
EnumMember,
67
Interface,
@@ -633,6 +634,16 @@ export class TypeEmitter<T, TOptions extends object = Record<string, never>> {
633634
return emittedSourceFile;
634635
}
635636

637+
async writeOutput(sourceFiles: SourceFile<T>[]) {
638+
for (const file of sourceFiles) {
639+
const outputFile = this.emitter.emitSourceFile(file);
640+
await emitFile(this.emitter.getProgram(), {
641+
path: outputFile.path,
642+
content: outputFile.contents,
643+
});
644+
}
645+
}
646+
636647
reference(
637648
targetDeclaration: Declaration<T>,
638649
pathUp: Scope<T>[],

packages/compiler/src/emitter-framework/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface AssetEmitter<T, TOptions extends object = Record<string, unknow
3939
emitEnumMembers(en: Enum): EmitEntity<T>;
4040
emitUnionVariants(union: Union): EmitEntity<T>;
4141
emitTupleLiteralValues(tuple: Tuple): EmitEntity<T>;
42-
42+
emitSourceFile(sourceFile: SourceFile<T>): EmittedSourceFile;
4343
/**
4444
* Create a source file.
4545
*
@@ -86,6 +86,7 @@ export interface SourceFile<T> {
8686
path: string;
8787
globalScope: Scope<T>;
8888
imports: Map<string, string[]>;
89+
meta: Record<string, any>;
8990
}
9091

9192
export interface EmittedSourceFile {
@@ -98,6 +99,7 @@ export type EmitEntity<T> = Declaration<T> | RawCode<T> | NoEmit | CircularEmit;
9899
export class EmitterResult {}
99100
export class Declaration<T> extends EmitterResult {
100101
public kind = "declaration" as const;
102+
public meta: Record<string, any> = {};
101103

102104
constructor(public name: string, public scope: Scope<T>, public value: T | Placeholder<T>) {
103105
if (value instanceof Placeholder) {

packages/json-schema/src/index.ts

+17
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,27 @@ export function findBaseUri(
7474
return baseUrl;
7575
}
7676

77+
export function isJsonSchemaDeclaration(program: Program, target: JsonSchemaDeclaration) {
78+
let current: JsonSchemaDeclaration | Namespace | undefined = target;
79+
do {
80+
if (getJsonSchema(program, current)) {
81+
return true;
82+
}
83+
84+
current = current.namespace;
85+
} while (current);
86+
87+
return false;
88+
}
89+
7790
export function getJsonSchemaTypes(program: Program): (Namespace | Model)[] {
7891
return [...(program.stateSet(jsonSchemaKey) || [])] as (Namespace | Model)[];
7992
}
8093

94+
export function getJsonSchema(program: Program, target: Type) {
95+
return program.stateSet(jsonSchemaKey).has(target);
96+
}
97+
8198
const multipleOfKey = createStateSymbol("JsonSchema.multipleOf");
8299
export function $multipleOf(context: DecoratorContext, target: Model, value: number) {
83100
context.program.stateMap(multipleOfKey).set(target, value);

0 commit comments

Comments
 (0)