diff --git a/packages/playground/package.json b/packages/playground/package.json index acf9b459764..145452a12ff 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -47,6 +47,7 @@ "@typespec/rest": "~0.43.0", "@typespec/openapi3": "~0.43.0", "@typespec/openapi": "~0.43.0", + "@typespec/protobuf": "~0.43.0", "@typespec/html-program-viewer": "~0.43.0", "@emotion/react": "^11.10.4", "@vitejs/plugin-react": "~3.1.0", diff --git a/packages/playground/samples/kiosk.tsp b/packages/playground/samples/kiosk.tsp new file mode 100644 index 00000000000..bfe9783847c --- /dev/null +++ b/packages/playground/samples/kiosk.tsp @@ -0,0 +1,101 @@ +import "@typespec/protobuf"; + +using TypeSpec.Protobuf; + +@package({ + name: "kiosk", +}) +namespace KioskExample; + +@TypeSpec.Protobuf.service +interface Display { + /** + * Create a new kiosk. This enrolls the kiosk for sign display. + */ + createKiosk(...Kiosk): Kiosk; + + /** + * List active kiosks. + */ + listKiosks(...WellKnown.Empty): { + @field(1) kiosks: Kiosk[]; + }; + + /** + * Get a kiosk. + */ + getKiosk(@field(1) id: int32): Kiosk; + + /** + * Delete a kiosk. + */ + deleteKiosk(@field(1) id: int32): void; + + /** + * Create a new sign. + */ + createSign(...Sign): Sign; + + /** + * List active signs. + */ + listSigns(...WellKnown.Empty): { + @field(1) signs: Sign[]; + }; + + /** + * Get a sign. + */ + getSign(@field(1) id: int32): Sign; + + /** + * Delete a sign. + */ + deleteSign(@field(1) id: int32): void; + + /** + * Set a sign for display on one or more kiosks + */ + setSignIdForKioskIds(@field(1) kiosk_ids: int32[], @field(2) sign_id: int32): void; + + /** + * Get the sign that should be displayed on a kiosk. + */ + getSignIdForKioskId(@field(1) kiosk_id: int32): GetSignIdResponse; + /** + * Get signs that should be displayed on a kiosk. Streams. + */ + @stream(StreamMode.Out) + getSignIdsforKioskId(@field(1) kiosk_id: int32): GetSignIdResponse; +} + +model Kiosk { + // Output only. + @field(1) id?: int32; + // Required. + @field(2) name: string; + @field(3) size: ScreenSize; + @field(4) location: WellKnown.LatLng; + // Output only. + @field(5) create_time?: WellKnown.Timestamp; +} + +model Sign { + // Output only. + @field(1) id?: int32; + // Required. + @field(2) name: string; + @field(3) text: string; + @field(4) image: bytes; + // Output only. + @field(5) create_time?: WellKnown.Timestamp; +} + +model ScreenSize { + @field(1) width: int32; + @field(2) height: int32; +} + +model GetSignIdResponse { + @field(1) sign_id: int32; +} diff --git a/packages/playground/src/build-utils/index.ts b/packages/playground/src/build-utils/index.ts index 40ba048b6ba..d835ace746d 100644 --- a/packages/playground/src/build-utils/index.ts +++ b/packages/playground/src/build-utils/index.ts @@ -63,11 +63,23 @@ function playgroundManifestPlugin(config: PlaygroundConfig): Plugin { load(id: string) { if (id === `playground-manifest.js`) { const sampleImport = Object.values(samples) - .map((path, index) => `import s${index} from "${viteConfig.root}/${path}?raw"`) + .map( + (sampleValue, index) => + `import s${index} from "${viteConfig.root}/${sampleValue.fileName}?raw"` + ) .join("\n"); const sampleObj = [ "{", - ...Object.keys(samples).map((label, index) => `${JSON.stringify(label)}: s${index}, `), + ...Object.entries(samples).map( + ([label, config], index) => + `${JSON.stringify(label)}: { + fileName: ${JSON.stringify(config.fileName)}, + preferredEmitter: ${ + config.preferredEmitter ? JSON.stringify(config.preferredEmitter) : "undefined" + }, + content: s${index} + }, ` + ), "}", ].join("\n"); diff --git a/packages/playground/src/components/editor-command-bar.tsx b/packages/playground/src/components/editor-command-bar.tsx index 381c29e2d76..73a61393235 100644 --- a/packages/playground/src/components/editor-command-bar.tsx +++ b/packages/playground/src/components/editor-command-bar.tsx @@ -1,13 +1,17 @@ import { Link, Toolbar, ToolbarButton, Tooltip } from "@fluentui/react-components"; import { Bug16Regular, Save16Regular } from "@fluentui/react-icons"; -import { FunctionComponent } from "react"; +import { FunctionComponent, useCallback } from "react"; +import { useSetRecoilState } from "recoil"; +import { SampleConfig } from "../index.js"; +import { PlaygroundManifest } from "../manifest.js"; +import { selectedEmitterState } from "../state.js"; import { EmitterDropdown } from "./emitter-dropdown.js"; import { SamplesDropdown } from "./samples-dropdown.js"; export interface EditorCommandBarProps { documentationUrl?: string; saveCode: () => Promise | void; - updateTypeSpec: (value: string) => Promise | void; + updateTypeSpec: (value: string) => void; newIssue: () => Promise | void; } export const EditorCommandBar: FunctionComponent = ({ @@ -23,6 +27,20 @@ export const EditorCommandBar: FunctionComponent = ({ ) : undefined; + + const setEmitter = useSetRecoilState(selectedEmitterState); + + const onSelectSample = useCallback( + (config: SampleConfig) => { + if (!config.content) throw new Error("Unreachable: sample has no 'content' property"); + + updateTypeSpec(config.content); + + setEmitter(config.preferredEmitter ?? PlaygroundManifest.defaultEmitter); + }, + [setEmitter, updateTypeSpec] + ); + return (
@@ -34,7 +52,7 @@ export const EditorCommandBar: FunctionComponent = ({ onClick={saveCode as any} /> - + {documentation}
diff --git a/packages/playground/src/components/emitter-dropdown.tsx b/packages/playground/src/components/emitter-dropdown.tsx index bcce23abc82..6ab413e0236 100644 --- a/packages/playground/src/components/emitter-dropdown.tsx +++ b/packages/playground/src/components/emitter-dropdown.tsx @@ -33,7 +33,7 @@ export const EmitterDropdown: FunctionComponent = () => { return ( diff --git a/packages/playground/src/components/openapi-output.tsx b/packages/playground/src/components/openapi-output.tsx index ced3515abbd..688e452f545 100644 --- a/packages/playground/src/components/openapi-output.tsx +++ b/packages/playground/src/components/openapi-output.tsx @@ -12,7 +12,7 @@ export interface OpenAPIOutputProps { export const OpenAPIOutput: FunctionComponent = (props) => { const [selected, setSelected] = useState<"raw" | "swagger-ui">("raw"); const options = [ - { label: "OpenAPI", value: "raw" }, + { label: "Output Files", value: "raw" }, { label: "Swagger UI", value: "swagger-ui" }, ]; diff --git a/packages/playground/src/components/playground.tsx b/packages/playground/src/components/playground.tsx index 72ae9e8c7b0..7d7a86e2265 100644 --- a/packages/playground/src/components/playground.tsx +++ b/packages/playground/src/components/playground.tsx @@ -52,6 +52,7 @@ const PlaygroundInternal: FunctionComponent = ({ const doCompile = useCallback(async () => { const content = typespecModel.getValue(); const typespecCompiler = await importTypeSpecCompiler(); + const state = await compile(host, content, selectedEmitter, emittersOptions); setCompilationStatus(state); if ("program" in state) { @@ -81,9 +82,14 @@ const PlaygroundInternal: FunctionComponent = ({ }, [updateTypeSpec]); useEffect(() => { - const disposable = typespecModel.onDidChangeContent(debounce(() => doCompile(), 200)); - return () => disposable.dispose(); + const debouncer = debounce(() => doCompile(), 200); + const disposable = typespecModel.onDidChangeContent(debouncer); + return () => { + debouncer.clear(); + disposable.dispose(); + }; }, [typespecModel, doCompile]); + useEffect(() => { void doCompile(); }, [doCompile]); diff --git a/packages/playground/src/components/samples-dropdown.tsx b/packages/playground/src/components/samples-dropdown.tsx index 16d5bc0bfb3..bb4a37216be 100644 --- a/packages/playground/src/components/samples-dropdown.tsx +++ b/packages/playground/src/components/samples-dropdown.tsx @@ -1,8 +1,9 @@ import { Select } from "@fluentui/react-components"; import { FunctionComponent, useCallback, useEffect, useState } from "react"; +import { SampleConfig } from "../index.js"; import { PlaygroundManifest } from "../manifest.js"; export interface SamplesDropdownProps { - onSelectSample: (content: string) => void; + onSelectSample: (content: SampleConfig) => void; } export const SamplesDropdown: FunctionComponent = ({ onSelectSample }) => { const [selected, setSelected] = useState(""); @@ -24,6 +25,7 @@ export const SamplesDropdown: FunctionComponent = ({ onSel const handleSelected = useCallback( (evt: any) => { setSelected(evt.target.value); + onSelectSample(PlaygroundManifest.samples[evt.target.value]); }, [onSelectSample] diff --git a/packages/playground/src/index.ts b/packages/playground/src/index.ts index 9c4f6f6e9e3..55ceb56e5f0 100644 --- a/packages/playground/src/index.ts +++ b/packages/playground/src/index.ts @@ -1,7 +1,7 @@ export interface PlaygroundConfig { defaultEmitter: string; libraries: string[]; - samples: Record; + samples: Record; enableSwaggerUI: boolean; links: { newIssue: string; @@ -9,5 +9,11 @@ export interface PlaygroundConfig { }; } +export interface SampleConfig { + fileName: string; + preferredEmitter?: string; + content?: string; +} + export { createBrowserHost } from "./browser-host.js"; export { Playground, PlaygroundProps } from "./components/playground.js"; diff --git a/packages/playground/vite.config.ts b/packages/playground/vite.config.ts index 480a41b73d6..351cdb6bdd7 100644 --- a/packages/playground/vite.config.ts +++ b/packages/playground/vite.config.ts @@ -10,12 +10,23 @@ const config = definePlaygroundViteConfig({ "@typespec/openapi", "@typespec/versioning", "@typespec/openapi3", + "@typespec/protobuf", ], samples: { - "API versioning": "samples/versioning.tsp", - "Discriminated unions": "samples/unions.tsp", - "HTTP service": "samples/http.tsp", - "REST framework": "samples/rest.tsp", + "API versioning": { + fileName: "samples/versioning.tsp", + preferredEmitter: "@typespec/openapi3", + }, + "Discriminated unions": { + fileName: "samples/unions.tsp", + preferredEmitter: "@typespec/openapi3", + }, + "HTTP service": { fileName: "samples/http.tsp", preferredEmitter: "@typespec/openapi3" }, + "REST framework": { fileName: "samples/rest.tsp", preferredEmitter: "@typespec/openapi3" }, + "Protobuf Kiosk": { + fileName: "samples/kiosk.tsp", + preferredEmitter: "@typespec/protobuf", + }, }, enableSwaggerUI: true, links: { diff --git a/packages/protobuf/lib/proto.tsp b/packages/protobuf/lib/proto.tsp index bea96e092ae..59268bc4989 100644 --- a/packages/protobuf/lib/proto.tsp +++ b/packages/protobuf/lib/proto.tsp @@ -1,4 +1,4 @@ -import "../dist/src/proto.js"; +import "../dist/src/index.js"; namespace TypeSpec.Protobuf; @@ -57,6 +57,13 @@ namespace WellKnown { * This model references `google.protobuf.Any` from `google/protobuf/any.proto`. */ model Any is Extern<"google/protobuf/any.proto", "google.protobuf.Any">; + + /** + * A latitude and longitude. + * + * This model references `google.type.LatLng` from `google/type/latlng.proto`. + */ + model LatLng is Extern<"google/type/latlng.proto", "google.type.LatLng">; } /** diff --git a/packages/protobuf/package.json b/packages/protobuf/package.json index 39fb2915f4e..2b9de3cde99 100644 --- a/packages/protobuf/package.json +++ b/packages/protobuf/package.json @@ -18,7 +18,7 @@ "protobuf", "grpc" ], - "main": "dist/src/lib.js", + "main": "dist/src/index.js", "type": "module", "tspMain": "lib/proto.tsp", "scripts": { diff --git a/packages/protobuf/src/index.ts b/packages/protobuf/src/index.ts new file mode 100644 index 00000000000..0dd36fbcc21 --- /dev/null +++ b/packages/protobuf/src/index.ts @@ -0,0 +1,7 @@ +import { TypeSpecProtobufLibrary } from "./lib.js"; + +export const namespace = "TypeSpec.Protobuf"; + +export * from "./proto.js"; + +export const $lib = TypeSpecProtobufLibrary; diff --git a/packages/protobuf/src/lib.ts b/packages/protobuf/src/lib.ts index bbf1d936fff..53f0ac77e39 100644 --- a/packages/protobuf/src/lib.ts +++ b/packages/protobuf/src/lib.ts @@ -141,8 +141,6 @@ export const TypeSpecProtobufLibrary = createTypeSpecLibrary({ export const { reportDiagnostic } = TypeSpecProtobufLibrary; -export { $onEmit } from "./proto.js"; - export type TypeSpecProtobufLibrary = typeof TypeSpecProtobufLibrary; const keys = [ diff --git a/packages/protobuf/src/proto.ts b/packages/protobuf/src/proto.ts index 3e17d03566d..5f5669a7645 100644 --- a/packages/protobuf/src/proto.ts +++ b/packages/protobuf/src/proto.ts @@ -41,8 +41,8 @@ const IMPLEMENTATION_RESERVED_RANGE = [19000, 19999] as const; /** * Defined in the [ProtoBuf Language Spec](https://developers.google.com/protocol-buffers/docs/reference/proto3-spec#identifiers). * - * ident = letter { letter | decimalDigit | "_" } - * fullIdent = ident { "." ident } + * ident = letter \{ letter | decimalDigit | "_" \} + * fullIdent = ident \{ "." ident \} */ export const PROTO_FULL_IDENT = /([a-zA-Z][a-zA-Z0-9_]*)+/; @@ -215,4 +215,3 @@ export async function $onValidate(program: Program) { } export const namespace = "TypeSpec.Protobuf"; -export { TypeSpecProtobufLibrary as $lib }; diff --git a/packages/protobuf/src/transform/index.ts b/packages/protobuf/src/transform/index.ts index 121febbe620..25d9bbd7aab 100644 --- a/packages/protobuf/src/transform/index.ts +++ b/packages/protobuf/src/transform/index.ts @@ -22,7 +22,6 @@ import { Type, Union, } from "@typespec/compiler"; -import { EOL } from "os"; import { map, matchType, @@ -350,7 +349,7 @@ function tspToProto(program: Program): ProtoFile[] { if (!emptyType) { throw new Error( - `Could not resolve the empty type: ${diagnostics.map(formatDiagnostic).join(EOL)}` + `Could not resolve the empty type: ${diagnostics.map(formatDiagnostic).join("\n")}` ); } @@ -569,7 +568,7 @@ function tspToProto(program: Program): ProtoFile[] { for (const [[type, diagnostics]] of entries) { if (!type) { - const diagnosticString = diagnostics.map(formatDiagnostic).join(EOL); + const diagnosticString = diagnostics.map(formatDiagnostic).join("\n"); throw new Error( `Failed to construct TypeSpec -> Protobuf scalar map. Unexpected failure to resolve TypeSpec scalar: ${diagnosticString}` );