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
9 changes: 9 additions & 0 deletions packages/emitter-framework/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
"./python": {
"import": "./dist/src/python/index.js"
},
"./graphql": {
"import": "./dist/src/graphql/index.js"
},
"./testing": {
"import": "./dist/src/testing/index.js"
}
Expand All @@ -55,6 +58,10 @@
"#python/*": {
"development": "./src/python/*",
"default": "./dist/src/python/*"
},
"#graphql/*": {
"development": "./src/graphql/*",
"default": "./dist/src/graphql/*"
}
},
"keywords": [],
Expand All @@ -64,13 +71,15 @@
"peerDependencies": {
"@alloy-js/core": "^0.22.0",
"@alloy-js/csharp": "^0.22.0",
"@alloy-js/graphql": "^0.1.0",
"@alloy-js/python": "^0.3.0",
"@alloy-js/typescript": "^0.22.0",
"@typespec/compiler": "workspace:^"
},
"devDependencies": {
"@alloy-js/cli": "^0.22.0",
"@alloy-js/core": "^0.22.0",
"@alloy-js/graphql": "^0.1.0",
"@alloy-js/python": "^0.3.0",
"@alloy-js/rollup-plugin": "^0.1.0",
"@alloy-js/typescript": "^0.22.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { For } from "@alloy-js/core";
import * as gql from "@alloy-js/graphql";
import type { Enum, Union } from "@typespec/compiler";
import { useTsp } from "../../core/context/tsp-context.js";
import { reportDiagnostic } from "../../lib.js";

export interface EnumDeclarationProps {
name?: string;
type: Union | Enum;
doc?: string;
}

export function EnumDeclaration(props: EnumDeclarationProps) {
const { $ } = useTsp();
let type: Enum;
if ($.union.is(props.type)) {
if (!$.union.isValidEnum(props.type)) {
throw new Error("The provided union type cannot be represented as an enum");
}
type = $.enum.createFromUnion(props.type);
} else {
type = props.type;
}

if (!props.type.name || props.type.name === "") {
reportDiagnostic($.program, { code: "type-declaration-missing-name", target: props.type });
}

const name = props.name ?? props.type.name!;
const members = Array.from(type.members.entries());
const doc = props.doc ?? $.type.getDoc(type) ?? undefined;

return (
<gql.EnumType name={name} description={doc}>
<For each={members}>
{([_key, value]) => {
const memberDoc = $.type.getDoc(value) ?? undefined;
return <gql.EnumValue name={value.name} description={memberDoc} />;
}}
</For>
</gql.EnumType>
);
}
5 changes: 5 additions & 0 deletions packages/emitter-framework/src/graphql/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./enum-declaration.js";
export * from "./object-type-declaration.js";
export * from "./type-declaration.js";
export * from "./type-expression.js";
export * from "./union-declaration.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { For } from "@alloy-js/core";
import * as gql from "@alloy-js/graphql";
import type { Model } from "@typespec/compiler";
import { isNeverType } from "@typespec/compiler";
import { useTsp } from "../../core/context/tsp-context.js";
import { getTypeReference } from "./type-expression.js";

export interface ObjectTypeDeclarationProps {
name?: string;
type: Model;
doc?: string;
}

export function ObjectTypeDeclaration(props: ObjectTypeDeclarationProps) {
const { $ } = useTsp();
const type = props.type;
const name = props.name ?? type.name!;
const doc = props.doc ?? $.type.getDoc(type) ?? undefined;
const properties = Array.from($.model.getProperties(type).values()).filter(
(prop) => !isNeverType(prop.type),
);

return (
<gql.ObjectType name={name} description={doc}>
<For each={properties}>
{(prop) => {
const propDoc = $.type.getDoc(prop) ?? undefined;
return (
<gql.Field
name={prop.name}
type={getTypeReference($, prop.type)}
nonNull={!prop.optional}
description={propDoc}
/>
);
}}
</For>
</gql.ObjectType>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Type } from "@typespec/compiler";
import { useTsp } from "../../core/context/tsp-context.js";
import { EnumDeclaration } from "./enum-declaration.js";
import { ObjectTypeDeclaration } from "./object-type-declaration.js";
import { UnionDeclaration } from "./union-declaration.js";

export interface TypeDeclarationProps {
name?: string;
type: Type;
doc?: string;
}

export function TypeDeclaration(props: TypeDeclarationProps) {
const { $ } = useTsp();
const { type, ...restProps } = props;
const doc = props.doc ?? $.type.getDoc(type) ?? undefined;

switch (type.kind) {
case "Model":
return <ObjectTypeDeclaration doc={doc} type={type} {...restProps} />;
case "Enum":
return <EnumDeclaration doc={doc} type={type} {...restProps} />;
case "Union":
if ($.union.isValidEnum(type)) {
return <EnumDeclaration doc={doc} type={type} {...restProps} />;
}
return <UnionDeclaration doc={doc} type={type} {...restProps} />;
}
}
172 changes: 172 additions & 0 deletions packages/emitter-framework/src/graphql/components/type-expression.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import type { IntrinsicType, Scalar, Type } from "@typespec/compiler";
import type { Typekit } from "@typespec/compiler/typekit";
import type { TypeReference } from "@alloy-js/graphql";
import { Experimental_OverridableComponent } from "../../core/components/overrides/component-overrides.jsx";
import { useTsp } from "../../core/context/tsp-context.js";
import { reportGraphqlDiagnostic } from "../lib.js";

export interface TypeExpressionProps {
type: Type;
}

export function TypeExpression(props: TypeExpressionProps) {
const { $ } = useTsp();
const type = props.type;

return (
<Experimental_OverridableComponent reference type={type}>
{() => {
switch (type.kind) {
case "Scalar":
case "Intrinsic":
return <>{getScalarIntrinsicExpression($, type)}</>;
case "Model":
if ($.array.is(type)) {
const elementType = type.indexer!.value;
return <TypeExpression type={elementType} />;
}
if ($.record.is(type)) {
reportGraphqlDiagnostic($.program, {
code: "graphql-unsupported-type",
target: type,
});
return <>String</>;
}
return <>{type.name}</>;
case "Enum":
return <>{type.name}</>;
case "Union":
return <>{type.name}</>;
case "UnionVariant":
return <TypeExpression type={type.type} />;
case "ModelProperty":
return <TypeExpression type={type.type} />;
default:
reportGraphqlDiagnostic($.program, {
code: "graphql-unsupported-type",
target: type,
});
return <>String</>;
}
}}
</Experimental_OverridableComponent>
);
}

const intrinsicNameToGraphQLType = new Map<string, string | null>([
// Core types
["string", "String"],
["boolean", "Boolean"],
["null", null], // Not representable in GraphQL
["void", null], // Not representable in GraphQL
["never", null], // Not representable in GraphQL
["unknown", null], // Not representable in GraphQL
["bytes", "String"], // Base64 encoded

// Numeric types - GraphQL Int is 32-bit signed
["numeric", "Int"], // Abstract parent type
["integer", "Int"], // Abstract parent type
["float", "Float"],
["decimal", "Float"], // No decimal in GraphQL
["decimal128", "Float"], // No decimal in GraphQL
["int64", "String"], // Too large for GraphQL Int
["int32", "Int"],
["int16", "Int"],
["int8", "Int"],
["safeint", "Int"],
["uint64", "String"], // Too large for GraphQL Int
["uint32", "Int"], // Borderline, keep as Int
["uint16", "Int"],
["uint8", "Int"],
["float32", "Float"],
["float64", "Float"],

// Date and time types - custom scalars could override
["plainDate", "String"],
["plainTime", "String"],
["utcDateTime", "String"],
["offsetDateTime", "String"],
["duration", "String"],

// String types
["url", "String"],
]);

function getScalarIntrinsicExpression($: Typekit, type: Scalar | IntrinsicType): string | null {
let intrinsicName: string;
if ($.scalar.is(type)) {
intrinsicName = $.scalar.getStdBase(type)?.name ?? "";
} else {
intrinsicName = type.name;
}

const gqlType = intrinsicNameToGraphQLType.get(intrinsicName);

if (gqlType === undefined) {
reportGraphqlDiagnostic($.program, { code: "graphql-unsupported-scalar", target: type });
return "String";
}

if (gqlType === null) {
reportGraphqlDiagnostic($.program, { code: "graphql-unsupported-type", target: type });
return null;
}

return gqlType;
}

/**
* Returns a GraphQL TypeReference for use in Field/InputField type props.
*/
export function getTypeReference($: Typekit, type: Type): TypeReference {
switch (type.kind) {
case "Scalar":
case "Intrinsic": {
const gqlType = getScalarIntrinsicExpression($, type);
return gqlType ?? "String";
}
case "Model":
if ($.array.is(type)) {
const elementType = type.indexer!.value;
return { kind: "list", ofType: getTypeReference($, elementType) };
}
if ($.record.is(type)) {
reportGraphqlDiagnostic($.program, {
code: "graphql-unsupported-type",
target: type,
});
return "String";
}
return type.name!;
case "Enum":
return type.name!;
case "Union":
return type.name!;
case "UnionVariant":
return getTypeReference($, type.type);
case "ModelProperty":
return getTypeReference($, type.type);
default:
reportGraphqlDiagnostic($.program, {
code: "graphql-unsupported-type",
target: type,
});
return "String";
}
}

export function isDeclaration($: Typekit, type: Type): boolean {
switch (type.kind) {
case "Model":
if ($.array.is(type) || $.record.is(type)) {
return false;
}
return Boolean(type.name);
case "Enum":
return true;
case "Union":
return Boolean(type.name);
default:
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as gql from "@alloy-js/graphql";
import type { Union } from "@typespec/compiler";
import { useTsp } from "../../core/context/tsp-context.js";
import { reportDiagnostic } from "../../lib.js";
import { reportGraphqlDiagnostic } from "../lib.js";

export interface UnionDeclarationProps {
name?: string;
type: Union;
doc?: string;
}

export function UnionDeclaration(props: UnionDeclarationProps) {
const { $ } = useTsp();
const type = props.type;

if (!type.name || type.name === "") {
reportDiagnostic($.program, { code: "type-declaration-missing-name", target: type });
}

const name = props.name ?? type.name!;
const doc = props.doc ?? $.type.getDoc(type) ?? undefined;

// GraphQL unions can only contain object types. Filter to named model members.
const validMembers: string[] = [];
for (const variant of type.variants.values()) {
const variantType = variant.type;
if (variantType.kind === "Model" && variantType.name && !$.array.is(variantType) && !$.record.is(variantType)) {
validMembers.push(variantType.name);
} else {
reportGraphqlDiagnostic($.program, {
code: "graphql-unsupported-type",
target: variant,
});
}
}

return (
<gql.UnionType name={name} description={doc} members={validMembers} />
);
}
2 changes: 2 additions & 0 deletions packages/emitter-framework/src/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./components/index.js";
export * from "./utils/index.js";
Loading