-
Notifications
You must be signed in to change notification settings - Fork 12
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
feat(metagen): client_ts #790
Conversation
@coderabbitai review |
WalkthroughThe changes encompass updates to Docker image references, enhancements for generating TypeScript and Python client code, and improvements in error handling for TypeGraph functions. New constants and configuration adjustments boost configurability and code management across multiple modules. Additionally, several new files have been introduced to support the extended functionalities, including client generation for various programming languages and improved testing capabilities. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant TypeScriptGenerator
participant GraphQLTransportCore
participant GraphQLTransport
User->>TypeScriptGenerator: Initialize with Config
TypeScriptGenerator->>GraphQLTransportCore: Create GraphQL Transport
GraphQLTransportCore-->>GraphQLTransport: Provide Transport Details
TypeScriptGenerator->>GraphQLTransport: Generate Code and Execute Queries
GraphQLTransport-->>TypeScriptGenerator: Return Query Results
TypeScriptGenerator-->>User: Provide Generated Code and Results
sequenceDiagram
participant User
participant TypeGraph
participant ErrorHandler
participant Builder
User->>TypeGraph: Call `typegraph` function
TypeGraph->>Builder: Execute Builder Function
Builder-->>TypeGraph: Success/Error
TypeGraph->>ErrorHandler: Handle Error (if any)
ErrorHandler-->>TypeGraph: Return Modified Error
TypeGraph-->>User: Return Result/Error
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (invoked as PR comments)
Additionally, you can add CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
Outside diff range, codebase verification and nitpick comments (6)
libs/metagen/src/client_ts/node_metas.rs (4)
1-3
: Add a brief file description.Consider adding a brief description of the file's purpose at the top.
+ // This module provides TypeScript node metadata rendering utilities.
15-15
: Add a detailed method description.Consider adding a detailed description of what this method does, including the parameters and return values.
+ /// Renders metadata for a TypeScript object node.
47-47
: Add a detailed method description.Consider adding a detailed description of what this method does, including the parameters and return values.
+ /// Renders metadata for a TypeScript function node.
92-92
: Add a detailed method description.Consider adding a detailed description of what this method does, including the parameters and return values.
+ /// Renders the metadata for the provided TypeNode.
libs/metagen/src/shared/types.rs (2)
10-10
: Consider adding documentation forVisitedTypePaths
.While the type definition is clear, adding a brief comment explaining its purpose would enhance maintainability.
27-33
: Consider adding documentation forVisitCursor
.Adding a brief comment explaining the purpose and usage of
VisitCursor
would enhance code readability and maintainability.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #790 +/- ##
==========================================
- Coverage 68.90% 68.87% -0.03%
==========================================
Files 144 144
Lines 16673 16680 +7
Branches 1473 1474 +1
==========================================
+ Hits 11488 11489 +1
- Misses 5159 5165 +6
Partials 26 26 ☔ View full report in Codecov by Sentry. |
Usage sample typescriptimport { alias, PreparedArgs, QueryGraph } from "./client.ts";
const api1 = new QueryGraph();
const gqlClient = api1.graphql(
`http://localhost:${Deno.env.get("TG_PORT")}/sample`,
);
const preparedQ = gqlClient.prepareQuery(() => ({
user: api1.getUser({
_: "selectAll",
posts: alias({
post1: { id: true, slug: true, title: true },
post2: { _: "selectAll", id: false },
}),
}),
posts: api1.getPosts({ _: "selectAll" }),
scalarNoArgs: api1.scalarNoArgs(),
}));
const preparedM = gqlClient.prepareMutation((
args: PreparedArgs<{
id: string;
slug: string;
title: string;
}>,
) => ({
scalarArgs: api1.scalarArgs({
id: args.get("id"),
slug: args.get("slug"),
title: args.get("title"),
}),
compositeNoArgs: api1.compositeNoArgs({
_: "selectAll",
}),
compositeArgs: api1.compositeArgs({
id: args.get("id"),
}, {
_: "selectAll",
}),
}));
const res1 = await preparedQ.perform({});
const res1a = await preparedQ.perform({});
const res2 = await preparedM.perform({
id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2",
slug: "s",
title: "t",
});
const res3 = await gqlClient.query({
user: api1.getUser({
_: "selectAll",
posts: alias({
post1: { id: true, slug: true, title: true },
post2: { _: "selectAll", id: false },
}),
}),
posts: api1.getPosts({ _: "selectAll" }),
scalarNoArgs: api1.scalarNoArgs(),
});
const res4 = await gqlClient.mutation({
scalarArgs: api1.scalarArgs({
id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2",
slug: "",
title: "",
}),
compositeNoArgs: api1.compositeNoArgs({
_: "selectAll",
}),
compositeArgs: api1.compositeArgs({
id: "94be5420-8c4a-4e67-b4f4-e1b2b54832a2",
}, {
_: "selectAll",
}),
});
console.log(JSON.stringify([res1, res1a, res2, res3, res4])); Generation sample typescript// This file was @generated by metagen and is intended
// to be generated again on subsequent metagen runs.
function _selectionToNodeSet(
selection: Selection,
metas: [string, () => NodeMeta][],
parentPath: string,
): SelectNode<unknown>[] {
const out = [] as SelectNode[];
const selectAll = selection._ == "selectAll";
// set of the user specified nodes to do sanity
// check at the end
const foundNodes = new Set(Object.keys(selection));
for (
const [nodeName, metaFn] of metas ?? []
) {
foundNodes.delete(nodeName);
const nodeSelection = selection[nodeName];
if (!nodeSelection && !selectAll) {
// this node was not selected
continue;
}
const { argumentTypes, subNodes } = metaFn();
const nodeInstances = nodeSelection instanceof Alias
? nodeSelection.aliases()
: { [nodeName]: nodeSelection };
for (
const [instanceName, instanceSelection] of Object.entries(nodeInstances)
) {
if (!instanceSelection && !selectAll) {
continue;
}
if (instanceSelection instanceof Alias) {
throw new Error(
`nested Alias discovored at ${parentPath}.${instanceName}`,
);
}
const node: SelectNode = { instanceName, nodeName };
if (argumentTypes) {
// make sure the arg is of the expected form
let arg = instanceSelection;
if (Array.isArray(arg)) {
arg = arg[0];
}
// TODO: consider bringing in Zod (after hoisting impl into common lib)
if (typeof arg != "object" || arg === null) {
throw new Error(
`node at ${parentPath}.${instanceName} is a node ` +
`that requires arguments object but detected argument ` +
`is typeof ${typeof arg}`,
);
}
const expectedArguments = new Map(Object.entries(argumentTypes));
node.args = {};
for (const [key, value] of Object.entries(arg)) {
const typeName = expectedArguments.get(key);
// TODO: consider logging a warning if `_` is detected incase user passes
// Selection as arg
if (!typeName) {
throw new Error(
`unexpected argument ${key} at ${parentPath}.${instanceName}`,
);
}
expectedArguments.delete(key);
node.args[key] = { typeName, value };
}
}
if (subNodes) {
// sanity check selection object
let subSelections = instanceSelection;
if (argumentTypes) {
if (!Array.isArray(subSelections)) {
throw new Error(
`node at ${parentPath}.${instanceName} ` +
`is a composite that takes an argument ` +
`but selection is typeof ${typeof subSelections}`,
);
}
subSelections = subSelections[1];
} else if (Array.isArray(subSelections)) {
throw new Error(
`node at ${parentPath}.${instanceName} ` +
`is a composite that takes no arguments ` +
`but selection is typeof ${typeof subSelections}`,
);
}
if (typeof subSelections != "object") {
throw new Error(
`node at ${parentPath}.${nodeName} ` +
`is a no argument composite but first element of ` +
`selection is typeof ${typeof nodeSelection}`,
);
}
node.subNodes = _selectionToNodeSet(
// assume it's a Selection. If it's an argument
// object, mismatch between the node desc should hopefully
// catch it
subSelections as Selection,
subNodes,
`${parentPath}.${instanceName}`,
);
}
out.push(node);
}
}
foundNodes.delete("_");
if (foundNodes.size > 0) {
throw new Error(
`unexpected nodes found in selection set at ${parentPath}: ${[
...foundNodes,
]}`,
);
}
return out;
}
/* Query node types section */
type SelectNode<_Out = unknown> = {
nodeName: string;
instanceName: string;
args?: NodeArgs;
subNodes?: SelectNode[];
};
export class QueryNode<Out> {
#inner: SelectNode<Out>;
constructor(
inner: SelectNode<Out>,
) {
this.#inner = inner;
}
inner() {
return this.#inner;
}
}
export class MutationNode<Out> {
#inner: SelectNode<Out>;
constructor(
inner: SelectNode<Out>,
) {
this.#inner = inner;
}
inner() {
return this.#inner;
}
}
type SelectNodeOut<T> = T extends (QueryNode<infer O> | MutationNode<infer O>)
? O
: never;
type QueryDocOut<T> = T extends
Record<string, QueryNode<unknown> | MutationNode<unknown>> ? {
[K in keyof T]: SelectNodeOut<T[K]>;
}
: never;
type NodeMeta = {
subNodes?: [string, () => NodeMeta][];
argumentTypes?: { [name: string]: string };
};
/* Selection types section */
type SelectionFlags = "selectAll";
type Selection = {
_?: SelectionFlags;
[key: string]:
| SelectionFlags
| ScalarSelectNoArgs
| ScalarSelectArgs<Record<string, unknown>>
| CompositeSelectNoArgs<Selection | undefined>
| CompositeSelectArgs<Record<string, unknown>, Selection>
| Selection;
};
type ScalarSelectNoArgs =
| boolean
| Alias<true>
| null
| undefined;
type ScalarSelectArgs<ArgT extends Record<string, unknown>> =
| ArgT
| PlaceholderArgs<ArgT>
| Alias<ArgT | PlaceholderArgs<ArgT>>
| false
| null
| undefined;
type CompositeSelectNoArgs<SelectionT> =
| SelectionT
| Alias<SelectionT>
| false
| null
| undefined;
type CompositeSelectArgs<ArgT extends Record<string, unknown>, SelectionT> =
| [ArgT | PlaceholderArgs<ArgT>, SelectionT]
| Alias<[ArgT | PlaceholderArgs<ArgT>, SelectionT]>
| false
| undefined
| null;
/**
* Request multiple instances of a single node under different
* aliases. Look at {@link alias} for a functional way of instantiating
* this class.
*/
export class Alias<T> {
#aliases: Record<string, T>;
constructor(
aliases: Record<string, T>,
) {
this.#aliases = aliases;
}
aliases() {
return this.#aliases;
}
}
/**
* Request multiple instances of a single node under different
* aliases.
*/
export function alias<T>(aliases: Record<string, T>): Alias<T> {
return new Alias(aliases);
}
/* Argument types section */
type NodeArgValue = {
typeName: string;
value: unknown;
};
type NodeArgs = {
[name: string]: NodeArgValue;
};
/**
* This object is passed to closures used for preparing requests
* ahead of time for {@link PreparedRequest}s. It allows one to
* get {@link PlaceholderValue}s that can be used in place of node
* arguments. At request time, the {@link PreparedRequest} then
* takes an object that adheres to `T` that can then be used
* to replace the placeholders.
*/
export class PreparedArgs<T extends Record<string, unknown>> {
get(key: OnlyStringKeys<T>): PlaceholderValue<T[typeof key]> {
return new PlaceholderValue(key);
}
}
/**
* Placeholder values for use by {@link PreparedRequest}
*/
export class PlaceholderValue<_T> {
#key: string;
constructor(key: string) {
this.#key = key;
}
key() {
return this.#key;
}
}
export type PlaceholderArgs<T extends Record<string, unknown>> = {
[K in keyof T]: PlaceholderValue<T[K]>;
};
/* GraphQL section */
/**
* Options to be used for requests performed by {@link GraphQLTransport}.
*/
export type GraphQlTransportOptions = Omit<RequestInit, "body"> & {
/**
* {@link fetch} implementaiton to use. Defaults to the one found in the environment
*/
fetch?: typeof fetch;
};
function convertQueryNodeGql(
node: SelectNode,
variables: Map<string, NodeArgValue>,
) {
let out = node.nodeName == node.instanceName
? node.nodeName
: `${node.instanceName}: ${node.nodeName}`;
const args = node.args;
if (args) {
out = `${out} (${
Object.entries(args)
.map(([key, val]) => {
const name = `in${variables.size}`;
variables.set(name, val);
return `${key}: $${name}`;
})
.join(", ")
})`;
}
const subNodes = node.subNodes;
if (subNodes) {
out = `${out} { ${
subNodes.map((node) => convertQueryNodeGql(node, variables)).join(" ")
} }`;
}
return out;
}
function buildGql(
typeToGqlTypeMap: Record<string, string>,
query: Record<string, SelectNode>,
ty: "query" | "mutation",
name: string = "",
) {
const variables = new Map<string, NodeArgValue>();
const rootNodes = Object
.entries(query)
.map(([key, node]) => {
const fixedNode = { ...node, instanceName: key };
return convertQueryNodeGql(fixedNode, variables);
})
.join("\n ");
let argsRow = [...variables.entries()]
.map(([key, val]) => `$${key}: ${typeToGqlTypeMap[val.typeName]}`)
.join(", ");
if (argsRow.length > 0) {
// graphql doesn't like empty parentheses so we only
// add them if there are args
argsRow = `(${argsRow})`;
}
const doc = `${ty} ${name}${argsRow} {
${rootNodes}
}`;
return {
doc,
variables: Object.fromEntries(
[...variables.entries()]
.map(([key, val]) => [key, val.value]),
),
};
}
async function fetchGql(
addr: URL,
doc: string,
variables: Record<string, unknown>,
options: GraphQlTransportOptions,
) {
// console.log(doc, variables);
const fetchImpl = options.fetch ?? fetch;
const res = await fetchImpl(addr, {
...options,
method: "POST",
headers: {
accept: "application/json",
"content-type": "application/json",
...options.headers ?? {},
},
body: JSON.stringify({
query: doc,
variables,
}),
});
if (!res.ok) {
const body = await res.text().catch((err) => `error reading body: ${err}`);
throw new (Error as ErrorPolyfill)(
`graphql request to ${addr} failed with status ${res.status}: ${body}`,
{
cause: {
response: res,
body,
},
},
);
}
if (res.headers.get("content-type") != "application/json") {
throw new (Error as ErrorPolyfill)(
"unexpected content type in response",
{
cause: {
response: res,
body: await res.text().catch((err) => `error reading body: ${err}`),
},
},
);
}
return await res.json() as { data: unknown; errors?: object[] };
}
/**
* Access the typegraph over it's exposed GraphQL API.
*/
export class GraphQLTransport {
constructor(
public address: URL,
public options: GraphQlTransportOptions,
private typeToGqlTypeMap: Record<string, string>,
) {
}
async #request(
doc: string,
variables: Record<string, unknown>,
options?: GraphQlTransportOptions,
) {
const res = await fetchGql(this.address, doc, variables, {
...this.options,
...options,
});
if ("errors" in res) {
throw new (Error as ErrorPolyfill)("graphql errors on response", {
cause: res.errors,
});
}
return res.data;
}
/**
* Make a query request to the typegraph.
*/
async query<Doc extends Record<string, QueryNode<unknown>>>(
query: Doc,
{ options, name = "" }: {
options?: GraphQlTransportOptions;
name?: string;
} = {},
): Promise<QueryDocOut<Doc>> {
const { variables, doc } = buildGql(
this.typeToGqlTypeMap,
Object.fromEntries(
Object.entries(query).map((
[key, val],
) => [key, (val as QueryNode<unknown>).inner()]),
),
"query",
name,
);
return await this.#request(doc, variables, options) as QueryDocOut<Doc>;
}
/**
* Make a mutation request to the typegraph.
*/
async mutation<Doc extends Record<string, MutationNode<unknown>>>(
query: Doc,
{ options, name = "" }: {
options?: GraphQlTransportOptions;
name?: string;
} = {},
): Promise<QueryDocOut<Doc>> {
const { variables, doc } = buildGql(
this.typeToGqlTypeMap,
Object.fromEntries(
Object.entries(query).map((
[key, val],
) => [key, (val as MutationNode<unknown>).inner()]),
),
"mutation",
name,
);
return await this.#request(doc, variables, options) as QueryDocOut<Doc>;
}
/**
* Prepare an ahead of time query {@link PreparedRequest}.
*/
prepareQuery<
T extends JsonObject,
Doc extends Record<string, QueryNode<unknown>>,
>(
fun: (args: PreparedArgs<T>) => Doc,
{ name = "" }: { name?: string } = {},
): PreparedRequest<T, Doc> {
return new PreparedRequest(
this.address,
this.options,
this.typeToGqlTypeMap,
fun,
"query",
name,
);
}
/**
* Prepare an ahead of time mutation {@link PreparedRequest}.
*/
prepareMutation<
T extends JsonObject,
Q extends Record<string, MutationNode<unknown>>,
>(
fun: (args: PreparedArgs<T>) => Q,
{ name = "" }: { name?: string } = {},
): PreparedRequest<T, Q> {
return new PreparedRequest(
this.address,
this.options,
this.typeToGqlTypeMap,
fun,
"mutation",
name,
);
}
}
/**
* Prepares the GraphQL string ahead of time and allows re-use
* avoid the compute and garbage overhead of re-building it for
* repeat queries.
*/
export class PreparedRequest<
T extends JsonObject,
Doc extends Record<string, QueryNode<unknown> | MutationNode<unknown>>,
> {
public doc: string;
#mappings: Record<string, unknown>;
constructor(
private address: URL,
private options: GraphQlTransportOptions,
typeToGqlTypeMap: Record<string, string>,
fun: (args: PreparedArgs<T>) => Doc,
ty: "query" | "mutation",
name: string = "",
) {
const args = new PreparedArgs<T>();
const dryRunNode = fun(args);
const { doc, variables } = buildGql(
typeToGqlTypeMap,
Object.fromEntries(
Object.entries(dryRunNode).map((
[key, val],
) => [key, (val as MutationNode<unknown>).inner()]),
),
ty,
name,
);
this.doc = doc;
this.#mappings = variables;
}
resolveVariables(
args: T,
mappings: Record<string, unknown>,
) {
const resolvedVariables = {} as Record<string, unknown>;
for (const [key, val] of Object.entries(mappings)) {
if (val instanceof PlaceholderValue) {
resolvedVariables[key] = args[val.key()];
} else if (typeof val == "object" && val != null) {
this.resolveVariables(args, val as JsonObject);
} else {
resolvedVariables[key] = val;
}
}
return resolvedVariables;
}
/**
* Execute the prepared request.
*/
async perform(args: T, opts?: GraphQlTransportOptions): Promise<
{
[K in keyof Doc]: SelectNodeOut<Doc[K]>;
}
> {
const resolvedVariables = this.resolveVariables(args, this.#mappings);
// console.log(this.doc, {
// resolvedVariables,
// mapping: this.#mappings,
// });
const res = await fetchGql(
this.address,
this.doc,
resolvedVariables,
{
...this.options,
...opts,
},
);
if ("errors" in res) {
throw new (Error as ErrorPolyfill)("graphql errors on response", {
cause: res.errors,
});
}
return res.data as QueryDocOut<Doc>;
}
}
/* Util types section */
type OnlyStringKeys<T extends Record<string, unknown>> = {
[K in keyof T]: K extends string ? K : never;
}[keyof T];
type JsonLiteral = string | number | boolean | null;
type JsonObject = { [key: string]: Json };
type JsonArray = Json[];
type Json = JsonLiteral | JsonObject | JsonArray;
type ErrorPolyfill = new (msg: string, payload: unknown) => Error;
/* QueryGraph section */
class _QueryGraphBase {
constructor(private typeNameMapGql: Record<string, string>) {}
/**
* Get the {@link GraphQLTransport} for the typegraph.
*/
graphql(addr: URL | string, options?: GraphQlTransportOptions) {
return new GraphQLTransport(
new URL(addr),
options ?? {},
this.typeNameMapGql,
);
}
}
// -------------------------------------------------- //
const nodeMetas = {
scalar() {
return {};
},
Post(): NodeMeta {
return {
subNodes: [
["id", nodeMetas.scalar],
["slug", nodeMetas.scalar],
["title", nodeMetas.scalar],
],
};
},
Func27(): NodeMeta {
return {
...nodeMetas.Post(),
};
},
Func25(): NodeMeta {
return {
...nodeMetas.scalar(),
};
},
Func24(): NodeMeta {
return {
...nodeMetas.Post(),
};
},
User(): NodeMeta {
return {
subNodes: [
["id", nodeMetas.scalar],
["email", nodeMetas.scalar],
["posts", nodeMetas.Post],
],
};
},
Func23(): NodeMeta {
return {
...nodeMetas.User(),
};
},
Func28(): NodeMeta {
return {
...nodeMetas.Post(),
argumentTypes: {
id: "String13",
},
};
},
Func26(): NodeMeta {
return {
...nodeMetas.scalar(),
argumentTypes: {
id: "String4",
slug: "String1",
title: "String1",
},
};
},
};
export type StringUuid = string;
export type Post = {
id: StringUuid;
slug: string;
title: string;
};
export type StringEmail = string;
export type Post7 = Array<Post>;
export type User = {
id: StringUuid;
email: StringEmail;
posts: Post7;
};
export type Object21 = {
id: string;
};
export type PostSelections = {
_?: SelectionFlags;
id?: ScalarSelectNoArgs;
slug?: ScalarSelectNoArgs;
title?: ScalarSelectNoArgs;
};
export type UserSelections = {
_?: SelectionFlags;
id?: ScalarSelectNoArgs;
email?: ScalarSelectNoArgs;
posts?: CompositeSelectNoArgs<PostSelections>;
};
export class QueryGraph extends _QueryGraphBase {
constructor() {
super({
"String4": "Any",
"String1": "Any",
"String13": "Any",
});
}
getUser(select: UserSelections) {
const inner = _selectionToNodeSet(
{ "getUser": select },
[["getUser", nodeMetas.Func23]],
"$q",
)[0];
return new QueryNode(inner) as QueryNode<User>;
}
getPosts(select: PostSelections) {
const inner = _selectionToNodeSet(
{ "getPosts": select },
[["getPosts", nodeMetas.Func24]],
"$q",
)[0];
return new QueryNode(inner) as QueryNode<Post>;
}
scalarNoArgs() {
const inner = _selectionToNodeSet(
{ "scalarNoArgs": true },
[["scalarNoArgs", nodeMetas.Func25]],
"$q",
)[0];
return new QueryNode(inner) as QueryNode<string>;
}
scalarArgs(args: Post | PlaceholderArgs<Post>) {
const inner = _selectionToNodeSet(
{ "scalarArgs": args },
[["scalarArgs", nodeMetas.Func26]],
"$q",
)[0];
return new MutationNode(inner) as MutationNode<string>;
}
compositeNoArgs(select: PostSelections) {
const inner = _selectionToNodeSet(
{ "compositeNoArgs": select },
[["compositeNoArgs", nodeMetas.Func27]],
"$q",
)[0];
return new MutationNode(inner) as MutationNode<Post>;
}
compositeArgs(
args: Object21 | PlaceholderArgs<Object21>,
select: PostSelections,
) {
const inner = _selectionToNodeSet(
{ "compositeArgs": [args, select] },
[["compositeArgs", nodeMetas.Func28]],
"$q",
)[0];
return new MutationNode(inner) as MutationNode<Post>;
}
} Usage sample pythonfrom client import (
QueryGraph,
PostSelections,
SelectionFlags,
UserSelections,
Alias,
)
import json
import os
qg = QueryGraph()
port = os.getenv("TG_PORT")
gql_client = qg.graphql_sync(f"http://localhost:{port}/sample")
prepared_q = gql_client.prepare_query(
lambda args: {
"user": qg.get_user(
UserSelections(
_=SelectionFlags(select_all=True),
posts=Alias(
post1=PostSelections(
id=True,
slug=True,
title=True,
),
post2=PostSelections(
_=SelectionFlags(select_all=True),
id=False,
),
),
),
),
"posts": qg.get_posts({"_": SelectionFlags(select_all=True)}),
"scalarNoArgs": qg.scalar_no_args(),
},
)
prepared_m = gql_client.prepare_mutation(
lambda args: {
"scalarArgs": qg.scalar_args(
{
"id": args.get("id"),
"slug": args.get("slug"),
"title": args.get("title"),
}
),
"compositeNoArgs": qg.composite_no_args({"_": SelectionFlags(select_all=True)}),
"compositeArgs": qg.composite_args(
{
"id": args.get("id"),
},
{"_": SelectionFlags(select_all=True)},
),
},
)
res1 = prepared_q.perform({})
res1a = prepared_q.perform({})
res2 = prepared_m.perform(
{
"id": "94be5420-8c4a-4e67-b4f4-e1b2b54832a2",
"slug": "s",
"title": "t",
}
)
res3 = gql_client.query(
{
"user": qg.get_user(
UserSelections(
_=SelectionFlags(select_all=True),
posts=Alias(
post1=PostSelections(
id=True,
slug=True,
title=True,
),
post2=PostSelections(
_=SelectionFlags(select_all=True),
id=False,
),
),
),
),
"posts": qg.get_posts({"_": SelectionFlags(select_all=True)}),
"scalarNoArgs": qg.scalar_no_args(),
}
)
res4 = gql_client.mutation(
{
"scalarArgs": qg.scalar_args(
{
"id": "94be5420-8c4a-4e67-b4f4-e1b2b54832a2",
"slug": "",
"title": "",
}
),
"compositeNoArgs": qg.composite_no_args({"_": SelectionFlags(select_all=True)}),
"compositeArgs": qg.composite_args(
{
"id": "94be5420-8c4a-4e67-b4f4-e1b2b54832a2",
},
{"_": SelectionFlags(select_all=True)},
),
}
)
print(json.dumps([res1, res1a, res2, res3, res4])) Generation sample python# This file was @generated by metagen and is intended
# to be generated again on subsequent metagen runs.
import typing
import dataclasses as dc
import json
import urllib.request as request
import urllib.error
import http.client as http_c
def selection_to_nodes(
selection: "SelectionGeneric",
metas: typing.Dict[str, typing.Callable[[], "NodeMeta"]],
parent_path: str,
) -> typing.List["SelectNode[typing.Any]"]:
out = []
flags = selection.get("_")
if flags is not None and not isinstance(flags, SelectionFlags):
raise Exception(
f"selection field '_' should be of type SelectionFlags but found {type(flags)}"
)
select_all = True if flags is not None and flags.select_all else False
found_nodes = set(selection.keys())
for node_name, meta_fn in metas.items():
found_nodes.discard(node_name)
node_selection = selection.get(node_name)
if node_selection is False or (node_selection is None and not select_all):
# this node was not selected
continue
meta = meta_fn()
# we splat out any aliasing of nodes here
node_instances = (
[(key, val) for key, val in node_selection.items.items()]
if isinstance(node_selection, Alias)
else [(node_name, node_selection)]
)
for instance_name, instance_selection in node_instances:
# print(parent_path, instance_selection, meta.sub_nodes, instance_selection, flags)
if instance_selection is False or (
instance_selection is None and not select_all
):
# this instance was not selected
continue
if isinstance(instance_selection, Alias):
raise Exception(
f"nested Alias node discovored at {parent_path}.{instance_name}"
)
instance_args: typing.Optional[NodeArgs] = None
if meta.arg_types is not None:
arg = instance_selection
if isinstance(arg, tuple):
arg = arg[0]
# arg types are always TypedDicts
if not isinstance(arg, dict):
raise Exception(
f"node at {parent_path}.{instance_name} is a node that "
+ "requires arguments "
+ f"but detected argument is typeof {type(arg)}"
)
# convert arg dict to NodeArgs
expected_args = {key: val for key, val in meta.arg_types.items()}
instance_args = {}
for key, val in arg.items():
ty_name = expected_args.pop(key)
if ty_name is None:
raise Exception(
f"unexpected argument ${key} at {parent_path}.{instance_name}"
)
instance_args[key] = NodeArgValue(ty_name, val)
sub_nodes: typing.Optional[typing.List[SelectNode]] = None
if meta.sub_nodes is not None:
sub_selections = instance_selection
# if node requires both selection and arg, it must be
# a CompositeSelectArgs which is a tuple selection
if meta.arg_types is not None:
if not isinstance(sub_selections, tuple):
raise Exception(
f"node at {parent_path}.{instance_name} is a composite "
+ "that requires an argument object "
+ f"but selection is typeof {type(sub_selections)}"
)
sub_selections = sub_selections[1]
elif isinstance(sub_selections, tuple):
raise Exception(
f"node at {parent_path}.{instance_name} "
+ "is a composite that takes no arguments "
+ f"but selection is typeof {type(instance_selection)}",
)
# selection types are always TypedDicts as well
if not isinstance(sub_selections, dict):
raise Exception(
f"node at {parent_path}.{instance_name} "
+ "is a no argument composite but first element of "
+ f"selection is typeof {type(instance_selection)}",
)
sub_nodes = selection_to_nodes(
typing.cast("SelectionGeneric", sub_selections),
meta.sub_nodes,
f"{parent_path}.{instance_name}",
)
node = SelectNode(node_name, instance_name, instance_args, sub_nodes)
out.append(node)
found_nodes.discard("_")
if len(found_nodes) > 0:
raise Exception(
f"unexpected nodes found in selection set at {parent_path}: {found_nodes}",
)
return out
# Util types section #
Out = typing.TypeVar("Out", covariant=True)
T = typing.TypeVar("T")
ArgT = typing.TypeVar("ArgT", bound=typing.Mapping[str, typing.Any])
SelectionT = typing.TypeVar("SelectionT")
# Query node types section #
@dc.dataclass
class SelectNode(typing.Generic[Out]):
node_name: str
instance_name: str
args: typing.Optional["NodeArgs"]
sub_nodes: typing.Optional[typing.List["SelectNode"]]
@dc.dataclass
class QueryNode(SelectNode[Out]):
pass
@dc.dataclass
class MutationNode(SelectNode[Out]):
pass
@dc.dataclass
class NodeMeta:
sub_nodes: typing.Optional[typing.Dict[str, typing.Callable[[], "NodeMeta"]]] = None
arg_types: typing.Optional[typing.Dict[str, str]] = None
# Argument types section #
@dc.dataclass
class NodeArgValue:
type_name: str
value: typing.Any
NodeArgs = typing.Dict[str, NodeArgValue]
class PlaceholderValue(typing.Generic[T]):
def __init__(self, key: str):
self.key = key
PlaceholderArgs = typing.Dict[str, PlaceholderValue]
class PreparedArgs:
def get(self, key: str) -> PlaceholderValue:
return PlaceholderValue(key)
# Selection types section #
class Alias(typing.Generic[SelectionT]):
"""
Request multiple instances of a single node under different
aliases.
"""
def __init__(self, **aliases: SelectionT):
self.items = aliases
ScalarSelectNoArgs = typing.Union[bool, Alias[typing.Literal[True]], None]
ScalarSelectArgs = typing.Union[
ArgT,
PlaceholderArgs,
Alias[typing.Union[ArgT, PlaceholderArgs]],
typing.Literal[False],
None,
]
CompositeSelectNoArgs = typing.Union[
SelectionT, Alias[SelectionT], typing.Literal[False], None
]
CompositeSelectArgs = typing.Union[
typing.Tuple[typing.Union[ArgT, PlaceholderArgs], SelectionT],
Alias[typing.Tuple[typing.Union[ArgT, PlaceholderArgs], SelectionT]],
typing.Literal[False],
None,
]
# FIXME: ideally this would be a TypedDict
# to allow full dict based queries but
# we need to reliably identify SelectionFlags at runtime
# but TypedDicts don't allow instanceof
@dc.dataclass
class SelectionFlags:
select_all: typing.Union[bool, None] = None
class Selection(typing.TypedDict, total=False):
_: SelectionFlags
SelectionGeneric = typing.Mapping[
str,
typing.Union[
SelectionFlags,
ScalarSelectNoArgs,
ScalarSelectArgs[typing.Mapping[str, typing.Any]],
CompositeSelectNoArgs["SelectionGeneric"],
# FIXME: should be possible to make SelectionT here `SelectionGeneric` recursively
# but something breaks
CompositeSelectArgs[typing.Mapping[str, typing.Any], typing.Any],
],
]
# GraphQL section
@dc.dataclass
class GraphQLTransportOptions:
headers: typing.Dict[str, str]
@dc.dataclass
class GraphQLRequest:
addr: str
method: str
headers: typing.Dict[str, str]
body: bytes
@dc.dataclass
class GraphQLResponse:
req: GraphQLRequest
status: int
headers: typing.Dict[str, str]
body: bytes
def convert_query_node_gql(
node: SelectNode,
variables: typing.Dict[str, NodeArgValue],
):
out = (
f"{node.instance_name}: {node.node_name}"
if node.instance_name != node.node_name
else node.node_name
)
if node.args is not None:
arg_row = ""
for key, val in node.args.items():
name = f"in{len(variables)}"
variables[name] = val
arg_row += f"{key}: ${name}, "
if len(arg_row):
out += f"({arg_row[:-2]})"
if node.sub_nodes is not None:
sub_node_list = ""
for node in node.sub_nodes:
sub_node_list += f"{convert_query_node_gql(node, variables)} "
out += f" {{ {sub_node_list}}}"
return out
class GraphQLTransportBase:
def __init__(
self,
addr: str,
opts: GraphQLTransportOptions,
ty_to_gql_ty_map: typing.Dict[str, str],
):
self.addr = addr
self.opts = opts
self.ty_to_gql_ty_map = ty_to_gql_ty_map
def build_gql(
self,
query: typing.Mapping[str, SelectNode],
ty: typing.Union[typing.Literal["query"], typing.Literal["mutation"]],
name: str = "",
):
variables: typing.Dict[str, NodeArgValue] = {}
root_nodes = ""
for key, node in query.items():
fixed_node = SelectNode(node.node_name, key, node.args, node.sub_nodes)
root_nodes += f" {convert_query_node_gql(fixed_node, variables)}\n"
args_row = ""
for key, val in variables.items():
args_row += f"${key}: {self.ty_to_gql_ty_map[val.type_name]}, "
if len(args_row):
args_row = f"({args_row[:-2]})"
doc = f"{ty} {name}{args_row} {{\n{root_nodes}}}"
return (doc, {key: val.value for key, val in variables.items()})
def build_req(
self,
doc: str,
variables: typing.Dict[str, typing.Any],
opts: typing.Optional[GraphQLTransportOptions] = None,
):
headers = {}
headers.update(self.opts.headers)
if opts:
headers.update(opts.headers)
headers.update(
{
"accept": "application/json",
"content-type": "application/json",
}
)
data = json.dumps({"query": doc, "variables": variables}).encode("utf-8")
return GraphQLRequest(
addr=self.addr,
method="POST",
headers=headers,
body=data,
)
def handle_response(self, res: GraphQLResponse):
if res.status != 200:
raise Exception(f"graphql request failed with status {res.status}", res)
if res.headers.get("content-type") != "application/json":
raise Exception("unexpected content-type in graphql response", res)
parsed = json.loads(res.body)
if parsed.get("errors"):
raise Exception("graphql errors in response", parsed)
return parsed["data"]
class GraphQLTransportUrlib(GraphQLTransportBase):
def fetch(
self,
doc: str,
variables: typing.Dict[str, typing.Any],
opts: typing.Optional[GraphQLTransportOptions],
):
req = self.build_req(doc, variables, opts)
try:
with request.urlopen(
request.Request(
url=req.addr, method=req.method, headers=req.headers, data=req.body
)
) as res:
http_res: http_c.HTTPResponse = res
return self.handle_response(
GraphQLResponse(
req,
status=http_res.status,
body=http_res.read(),
headers={key: val for key, val in http_res.headers.items()},
)
)
except request.HTTPError as res:
return self.handle_response(
GraphQLResponse(
req,
status=res.status or 599,
body=res.read(),
headers={key: val for key, val in res.headers.items()},
)
)
except urllib.error.URLError as err:
raise Exception(f"URL error: {err.reason}")
def query(
self,
inp: typing.Dict[str, QueryNode[Out]],
opts: typing.Optional[GraphQLTransportOptions] = None,
name: str = "",
) -> typing.Dict[str, Out]:
doc, variables = self.build_gql(
{key: val for key, val in inp.items()}, "query", name
)
# print(doc,variables)
# return {}
return self.fetch(doc, variables, opts)
def mutation(
self,
inp: typing.Dict[str, MutationNode[Out]],
opts: typing.Optional[GraphQLTransportOptions] = None,
name: str = "",
) -> typing.Dict[str, Out]:
doc, variables = self.build_gql(
{key: val for key, val in inp.items()}, "mutation", name
)
return self.fetch(doc, variables, opts)
def prepare_query(
self,
fun: typing.Callable[[PreparedArgs], typing.Dict[str, QueryNode[Out]]],
name: str = "",
) -> "PreparedRequestUrlib[Out]":
return PreparedRequestUrlib(self, fun, "query", name)
def prepare_mutation(
self,
fun: typing.Callable[[PreparedArgs], typing.Dict[str, MutationNode[Out]]],
name: str = "",
) -> "PreparedRequestUrlib[Out]":
return PreparedRequestUrlib(self, fun, "mutation", name)
class PreparedRequestBase(typing.Generic[Out]):
def __init__(
self,
transport: GraphQLTransportBase,
fun: typing.Callable[[PreparedArgs], typing.Mapping[str, SelectNode[Out]]],
ty: typing.Union[typing.Literal["query"], typing.Literal["mutation"]],
name: str = "",
):
dry_run_node = fun(PreparedArgs())
doc, variables = transport.build_gql(dry_run_node, ty, name)
self.doc = doc
self._mapping = variables
self.transport = transport
def resolve_vars(
self,
args: typing.Mapping[str, typing.Any],
mappings: typing.Dict[str, typing.Any],
):
resolved: typing.Dict[str, typing.Any] = {}
for key, val in mappings.items():
if isinstance(val, PlaceholderValue):
resolved[key] = args[val.key]
elif isinstance(val, dict):
self.resolve_vars(args, val)
else:
resolved[key] = val
return resolved
class PreparedRequestUrlib(PreparedRequestBase[Out]):
def __init__(
self,
transport: GraphQLTransportUrlib,
fun: typing.Callable[[PreparedArgs], typing.Mapping[str, SelectNode[Out]]],
ty: typing.Union[typing.Literal["query"], typing.Literal["mutation"]],
name: str = "",
):
super().__init__(transport, fun, ty, name)
self.transport = transport
def perform(
self,
args: typing.Mapping[str, typing.Any],
opts: typing.Optional[GraphQLTransportOptions] = None,
) -> typing.Dict[str, Out]:
resolved_vars = self.resolve_vars(args, self._mapping)
return self.transport.fetch(self.doc, resolved_vars, opts)
# Query graph section #
class QueryGraphBase:
def __init__(self, ty_to_gql_ty_map: typing.Dict[str, str]):
self.ty_to_gql_ty_map = ty_to_gql_ty_map
def graphql_sync(
self, addr: str, opts: typing.Optional[GraphQLTransportOptions] = None
):
return GraphQLTransportUrlib(
addr, opts or GraphQLTransportOptions({}), self.ty_to_gql_ty_map
)
# - - - - - - - - - -- - - - - - - -- - - #
class NodeDescs:
@staticmethod
def scalar():
return NodeMeta()
@staticmethod
def Post():
return NodeMeta(
sub_nodes={
"id": NodeDescs.scalar,
"slug": NodeDescs.scalar,
"title": NodeDescs.scalar,
},
)
@staticmethod
def User():
return NodeMeta(
sub_nodes={
"id": NodeDescs.scalar,
"email": NodeDescs.scalar,
"posts": NodeDescs.Post,
},
)
@staticmethod
def Func23():
return NodeMeta(
sub_nodes=NodeDescs.User().sub_nodes,
)
@staticmethod
def Func25():
return NodeMeta(
sub_nodes=NodeDescs.scalar().sub_nodes,
)
@staticmethod
def Func26():
return NodeMeta(
sub_nodes=NodeDescs.scalar().sub_nodes,
arg_types={
"id": "String4",
"slug": "String1",
"title": "String1",
},
)
@staticmethod
def Func27():
return NodeMeta(
sub_nodes=NodeDescs.Post().sub_nodes,
)
@staticmethod
def Func28():
return NodeMeta(
sub_nodes=NodeDescs.Post().sub_nodes,
arg_types={
"id": "String13",
},
)
@staticmethod
def Func24():
return NodeMeta(
sub_nodes=NodeDescs.Post().sub_nodes,
)
Object21 = typing.TypedDict(
"Object21",
{
"id": str,
},
total=False,
)
StringUuid = str
Post = typing.TypedDict(
"Post",
{
"id": StringUuid,
"slug": str,
"title": str,
},
total=False,
)
StringEmail = str
Post7 = typing.List[Post]
User = typing.TypedDict(
"User",
{
"id": StringUuid,
"email": StringEmail,
"posts": Post7,
},
total=False,
)
PostSelections = typing.TypedDict(
"PostSelections",
{
"_": SelectionFlags,
"id": ScalarSelectNoArgs,
"slug": ScalarSelectNoArgs,
"title": ScalarSelectNoArgs,
},
total=False,
)
UserSelections = typing.TypedDict(
"UserSelections",
{
"_": SelectionFlags,
"id": ScalarSelectNoArgs,
"email": ScalarSelectNoArgs,
"posts": CompositeSelectNoArgs["PostSelections"],
},
total=False,
)
class QueryGraph(QueryGraphBase):
def __init__(self):
super().__init__(
{
"String4": "Any",
"String1": "Any",
"String13": "Any",
}
)
def get_user(self, select: UserSelections) -> QueryNode[User]:
node = selection_to_nodes(
{"getUser": select}, {"getUser": NodeDescs.Func23}, "$q"
)[0]
return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes)
def get_posts(self, select: PostSelections) -> QueryNode[Post]:
node = selection_to_nodes(
{"getPosts": select}, {"getPosts": NodeDescs.Func24}, "$q"
)[0]
return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes)
def scalar_no_args(self) -> QueryNode[str]:
node = selection_to_nodes(
{"scalarNoArgs": True}, {"scalarNoArgs": NodeDescs.Func25}, "$q"
)[0]
return QueryNode(node.node_name, node.instance_name, node.args, node.sub_nodes)
def scalar_args(
self, args: typing.Union[Post, PlaceholderArgs]
) -> MutationNode[str]:
node = selection_to_nodes(
{"scalarArgs": args}, {"scalarArgs": NodeDescs.Func26}, "$q"
)[0]
return MutationNode(
node.node_name, node.instance_name, node.args, node.sub_nodes
)
def composite_no_args(self, select: PostSelections) -> MutationNode[Post]:
node = selection_to_nodes(
{"compositeNoArgs": select}, {"compositeNoArgs": NodeDescs.Func27}, "$q"
)[0]
return MutationNode(
node.node_name, node.instance_name, node.args, node.sub_nodes
)
def composite_args(
self, args: typing.Union[Object21, PlaceholderArgs], select: PostSelections
) -> MutationNode[Post]:
node = selection_to_nodes(
{"compositeArgs": (args, select)}, {"compositeArgs": NodeDescs.Func28}, "$q"
)[0]
return MutationNode(
node.node_name, node.instance_name, node.args, node.sub_nodes
) |
- Implements `client_py` as described in #777 This is a stacked PR on top of #790. #### Migration notes ... - [ ] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change --------- Co-authored-by: Teo Stocco <[email protected]> Co-authored-by: Estifanos Bireda <[email protected]> Co-authored-by: FITAHIANA Nomeniavo joe <[email protected]>
9cfa939
to
b1c54b3
Compare
commit 25d96a8 Author: Yohe-Am <[email protected]> Date: Wed Aug 7 07:21:33 2024 +0300 refactor: PreparedArgs api commit 278bc09 Author: Yohe-Am <[email protected]> Date: Wed Aug 7 03:10:16 2024 +0300 test: alias tests commit 8ff17ec Author: Yohe-Am <[email protected]> Date: Fri Aug 2 08:46:56 2024 +0300 fix: test commit 62d534d Merge: d1c532e 4c30067 Author: Yohe-Am <[email protected]> Date: Fri Aug 2 06:46:33 2024 +0300 Merge branch 'main' into feat/MET-567/python-gen-client commit d1c532e Author: Yohe-Am <[email protected]> Date: Fri Aug 2 04:30:28 2024 +0300 tests: client_py commit 676bdb8 Author: Yohe-Am <[email protected]> Date: Fri Aug 2 03:58:03 2024 +0300 tests: client_py commit 4c30067 Author: Yohe-Am <[email protected]> Date: Thu Aug 1 11:36:14 2024 +0300 chore: prepare 0.4.6 (#795) - Bump version to 0.4.6-0 - Add sanity tests for published SDKs - Bump deno to 1.45.2 - Bump rust to 1.79.0 - Fix myriad of bugs #### Migration notes ... - [x] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced new logging capabilities in the `ConnectedEngine` with adjustable logging levels. - Implemented cleanup procedures in tests to enhance resource management. - **Bug Fixes** - Fixed import paths for permissions to ensure correct functionality in tests and applications. - **Version Updates** - Incremented version numbers across multiple projects and packages to reflect ongoing development and improvements. - **Documentation** - Added comments to clarify code behavior and potential future considerations in various modules. - **Refactor** - Optimized string handling in several functions and adjusted method signatures for improved clarity and efficiency. <!-- end of auto-generated comment: release notes by coderabbit.ai --> commit f0ecfb5 Author: Yohe-Am <[email protected]> Date: Thu Aug 1 08:01:10 2024 +0300 wip: wip commit a48203a Author: Yohe-Am <[email protected]> Date: Thu Aug 1 06:15:12 2024 +0300 wip: wip commit bfee2f2 Author: Yohe-Am <[email protected]> Date: Thu Aug 1 05:22:51 2024 +0300 wip: wip commit e57c322 Author: Yohe-Am <[email protected]> Date: Wed Jul 31 19:16:23 2024 +0300 wip: wip commit f55a3e7 Author: Yohe-Am <[email protected]> Date: Wed Jul 31 13:37:01 2024 +0300 wip: wip commit 25d5c24 Author: FITAHIANA Nomeniavo joe <[email protected]> Date: Thu Jul 25 20:59:22 2024 +0300 feat: better arg split logic for MCLI_LOADER (#799) <!-- Pull requests are squashed and merged using: - their title as the commit message - their description as the commit body Having a good title and description is important for the users to get readable changelog. --> <!-- 1. Explain WHAT the change is about --> - <!-- 2. Explain WHY the change cannot be made simpler --> - <!-- 3. Explain HOW users should update their code --> #### Migration notes ... - [ ] The change comes with new or modified tests - [ ] Hard-to-understand functions have explanatory comments - [ ] End-user documentation is updated to reflect the change
@coderabbitai review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 21
Outside diff range, codebase verification and nitpick comments (14)
libs/metagen/src/client_py/utils.rs (1)
20-32
: TODO for Cleaning Non-Valid Characters Remains UnaddressedThe
normalize_type_title
function is used in various parts of the codebase, but there is no evidence of additional handling for non-valid characters. The TODO comment in the function should be addressed to ensure complete normalization.Analysis chain
Normalization of type titles is well-implemented, but address TODO.
The
normalize_type_title
function effectively transforms titles to PascalCase and moves leading numbers to the end. However, the TODO comment about cleaning non-valid characters should be addressed to ensure complete normalization.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify usage of `normalize_type_title` function and check for handling of non-valid characters. # Test: Search for function usage. Expect: Ensure non-valid characters are handled. ast-grep --lang rust --pattern 'normalize_type_title($_)' --jsonLength of output: 22570
Script:
#!/bin/bash # Search for comments or functions related to cleaning non-valid characters in the files where `normalize_type_title` is used. rg --context 5 "clean" libs/metagen/src/client_ts/selections.rs libs/metagen/src/client_ts/node_metas.rs libs/metagen/src/client_ts/mod.rs libs/metagen/src/mdk_typescript/types.rs libs/metagen/src/client_rs/node_metas.rs libs/metagen/src/client_rs/selections.rs libs/metagen/src/client_rs/mod.rs libs/metagen/src/client_py/mod.rs libs/metagen/src/client_py/selections.rs libs/metagen/src/client_py/node_metas.rs libs/metagen/src/mdk_rust/stubs.rs libs/metagen/src/client_py/types.rs libs/metagen/src/mdk_rust/types.rsLength of output: 517
libs/metagen/fixtures/client_ts/main.ts (1)
47-48
: Avoid redundant calls topreparedQ.perform()
.The calls to
preparedQ.perform({})
on lines 47 and 48 are identical. If both are necessary for testing different scenarios, consider adding comments to clarify their purpose. Otherwise, remove the redundancy.- const res1 = await preparedQ.perform({}); - const res1a = await preparedQ.perform({}); + const res1 = await preparedQ.perform({});typegate/tests/metagen/typegraphs/sample/ts/main.ts (1)
47-48
: Avoid redundant calls topreparedQ.perform()
.The calls to
preparedQ.perform({})
on lines 47 and 48 are identical. If both are necessary for testing different scenarios, consider adding comments to clarify their purpose. Otherwise, remove the redundancy.- const res1 = await preparedQ.perform({}); - const res1a = await preparedQ.perform({}); + const res1 = await preparedQ.perform({});libs/metagen/src/tests/fixtures.rs (1)
10-10
: Update file path intest_typegraph_1
.The file
tg.ts
is located atlibs/metagen/fixtures/tg.ts
. Ensure that the command intest_typegraph_1
uses this full path to correctly reference the file.
- Update command to use
libs/metagen/fixtures/tg.ts
instead offixtures/tg.ts
.Analysis chain
Update file path in
test_typegraph_1
.The file path in the
cargo run
command has been changed fromtests/tg.ts
tofixtures/tg.ts
. This likely reflects a reorganization of test files. Ensure that the new path is correct and accessible.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify the existence and accessibility of the updated file path. # Test: Check if the file `fixtures/tg.ts` exists. # Expect: File should exist. fd 'tg.ts' --search-path='fixtures'Length of output: 129
Script:
#!/bin/bash # Search for the file `tg.ts` in the entire repository to find its correct location. fd 'tg.ts'Length of output: 38
libs/metagen/fixtures/client_rs/main.rs (2)
104-132
: Consider logging the results for better traceability.The code prints the results of multiple GraphQL operations. Consider adding logging to capture these results for better traceability and debugging.
// Example: Adding logging for results println!("GraphQL operation results: {}", serde_json::to_string_pretty(&serde_json::json!([ // existing code... ]))?);
10-12
: EnsureTG_PORT
environment variable is setThe
TG_PORT
environment variable is not currently set, which is necessary for constructing the API endpoint URL. Please ensure that this environment variable is configured in all environments where the code will be executed.Analysis chain
Ensure environment variable
TG_PORT
is set.The code retrieves the
TG_PORT
environment variable, which is crucial for constructing the API endpoint. Ensure that this environment variable is set in all environments where this code will run.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify that the TG_PORT environment variable is set. # Test: Check if TG_PORT is set. Expect: Non-empty output if set. echo "TG_PORT: ${TG_PORT:-not set}"Length of output: 42
typegate/tests/metagen/typegraphs/identities/rs/mdk.rs (1)
112-112
: Inconsistent MT_VERSION Across CodebaseThe
MT_VERSION
variable has been updated to"0.4.8-0"
intypegate/tests/metagen/typegraphs/identities/rs/mdk.rs
. However, other files, such astypegate/tests/runtimes/wasm_wire/rust/mdk.rs
, still use"0.4.6"
, andlibs/metagen/src/mdk_rust/static/mdk.rs
uses a placeholder. To ensure compatibility, please update all instances ofMT_VERSION
to"0.4.8-0"
.
typegate/tests/runtimes/wasm_wire/rust/mdk.rs
: Update from"0.4.6"
to"0.4.8-0"
.libs/metagen/src/mdk_rust/static/mdk.rs
: Replace the placeholder with"0.4.8-0"
.Analysis chain
Update MT_VERSION to 0.4.8-0.
The static
MT_VERSION
has been updated to"0.4.8-0"
, reflecting a newer version for compatibility checks. Ensure that all dependencies and integrations align with this version change.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify usage of MT_VERSION across the codebase to ensure compatibility. # Test: Search for MT_VERSION usage. Expect: Consistent usage with the updated version. rg --type rust 'MT_VERSION'Length of output: 1235
typegate/tests/metagen/metagen_test.ts (1)
11-12
: Consider Importing Only Used Functions fromdax
andzod
.To improve clarity and potentially reduce bundle size, consider importing only the specific functions or classes used from
dax
andzod
.libs/metagen/src/client_rs/mod.rs (2)
21-32
: Validate Configuration Struct Fields.The
ClienRsGenConfig
struct usesgarde
for validation. Ensure that all fields are correctly validated and consider adding documentation for each field to clarify its purpose.
34-45
: Enhance Error Handling forfrom_json
MethodThe current implementation of the
from_json
method relies onanyhow
for error propagation without additional context or logging. Consider enhancing the error handling by adding more informative error messages or logging to aid in debugging. This will make the method more robust and user-friendly.
- File:
libs/metagen/src/client_rs/mod.rs
- Method:
from_json
Analysis chain
Ensure Robust Error Handling in
from_json
.The
from_json
method should handle potential errors robustly, especially when dealing with file paths and JSON parsing. Consider logging errors or providing more context in error messages.Scripts executed
The following scripts were executed for the analysis:
Script:
#!/bin/bash # Description: Verify error handling in `from_json`. # Test: Check for error handling in the `from_json` method. Expect: Robust handling and informative error messages. rg --type rust -A 5 'fn from_json' libs/metagen/src/client_rs/mod.rsLength of output: 395
Script:
#!/bin/bash # Search for logging or additional context handling related to the `from_json` method. rg 'from_json' -A 5 --type rustLength of output: 6004
libs/metagen/src/client_ts/static/mod.ts (1)
369-369
: Consider removing the commented-out console log.The commented-out console log might be useful for debugging but should be removed in production code.
- // console.log(doc, variables);
libs/metagen/src/mdk_rust/types.rs (2)
93-94
: Clarify the purpose ofbody_required
.The variable
body_required
is used to determine if a type alias should be generated. Consider adding a comment to explain its role in the rendering process.// Determine if a type alias is necessary based on the node's requirements let body_required = type_body_required(cursor.node.clone());
103-108
: Typographical errors in comments.There are typographical errors in the comments, such as "primitie" and "defualt". Correct these to improve readability.
// if [type_body_required] says so, we usually need to generate // aliases for even simple primitive types // under certain conditions, we don't want to generate aliases // for primitive types. this includes // - types with default generated names // - types with no special semanticslibs/metagen/src/client_rs/static/client.rs (1)
1188-1230
: Improve error messages inhandle_response
.The error messages could be more descriptive to aid debugging. Consider including more context, such as the response content or the URL.
return Err(GraphQLRequestError::RequestFailed { response: GraphQLResponse { status: response.status, headers: response.headers.clone(), body: response.body.clone(), }, message: format!("Request failed for URL: {}", response.url), });
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Massive, lgtm 👍 For the docs update, I suppose you will update the previous PR about them?
client_ts
as described in docs:/docs/reference/typegraph/client
#777 .Migration notes
...
Summary by CodeRabbit
Bug Fixes
typegraph
function to provide better error messages.Chores
docker.io
prefix for consistency.GHJK_VERSION
to reflect a semantic versioning format.New Features
metagen
library, enhancing client generation capabilities.test_typegraph_3
function for improved testing capabilities.