Skip to content

Commit b19fae4

Browse files
authored
Merge pull request #23 from hyperweb-io/feat/nullable-args
Parse nullable args
2 parents ce171b3 + fd7d56e commit b19fae4

File tree

5 files changed

+407
-2
lines changed

5 files changed

+407
-2
lines changed

packages/parse/__tests__/ContractAnalyzer.multiFile.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1188,4 +1188,92 @@ describe('ContractAnalyzer - Multi-File Analysis', () => {
11881188
]);
11891189
});
11901190
});
1191+
1192+
describe('null and undefined union types', () => {
1193+
it('should handle multi-file contract with null union parameters', () => {
1194+
const sourceFiles = {
1195+
'src/types.ts': `
1196+
export interface UserData {
1197+
id: string;
1198+
name: string | null;
1199+
}
1200+
`,
1201+
'src/contract.ts': `
1202+
import { UserData } from './types';
1203+
1204+
export default class NullableContract {
1205+
state = { users: [] as UserData[] };
1206+
1207+
addUser(userData: UserData | null): boolean {
1208+
if (userData) {
1209+
this.state.users.push(userData);
1210+
return true;
1211+
}
1212+
return false;
1213+
}
1214+
1215+
findUser(id: string | null): UserData | null {
1216+
return id ? this.state.users.find(u => u.id === id) || null : null;
1217+
}
1218+
}
1219+
`,
1220+
};
1221+
1222+
const result = analyzer.analyzeMultiFile(sourceFiles);
1223+
expect(result.queries).toEqual([
1224+
{
1225+
name: 'findUser',
1226+
params: [{ name: 'id', type: 'string | null' }],
1227+
returnType: 'UserData | null',
1228+
},
1229+
]);
1230+
expect(result.mutations).toEqual([
1231+
{
1232+
name: 'addUser',
1233+
params: [{ name: 'userData', type: 'UserData | null' }],
1234+
returnType: 'boolean',
1235+
},
1236+
]);
1237+
});
1238+
1239+
it('should handle multi-file contract with undefined union parameters', () => {
1240+
const sourceFiles = {
1241+
'src/models.ts': `
1242+
export type OptionalString = string | undefined;
1243+
export type NullableNumber = number | null | undefined;
1244+
`,
1245+
'src/contract.ts': `
1246+
import { OptionalString, NullableNumber } from './models';
1247+
1248+
export default class UndefinedContract {
1249+
state = { data: {} };
1250+
1251+
processValue(value: OptionalString): string {
1252+
return value || 'default';
1253+
}
1254+
1255+
updateCounter(increment: NullableNumber): void {
1256+
this.state.data.counter = (this.state.data.counter || 0) + (increment || 0);
1257+
}
1258+
}
1259+
`,
1260+
};
1261+
1262+
const result = analyzer.analyzeMultiFile(sourceFiles);
1263+
expect(result.queries).toEqual([
1264+
{
1265+
name: 'processValue',
1266+
params: [{ name: 'value', type: 'OptionalString' }],
1267+
returnType: 'string',
1268+
},
1269+
]);
1270+
expect(result.mutations).toEqual([
1271+
{
1272+
name: 'updateCounter',
1273+
params: [{ name: 'increment', type: 'NullableNumber' }],
1274+
returnType: 'void',
1275+
},
1276+
]);
1277+
});
1278+
});
11911279
});

packages/parse/__tests__/ContractAnalyzer.multiFileSchema.test.ts

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ describe('ContractAnalyzer - Multi-File with Schema', () => {
453453
{
454454
name: 'checkStatus',
455455
params: [],
456-
returnSchema: { anyOf: [{ type: 'boolean' }, {}] },
456+
returnSchema: { anyOf: [{ type: 'boolean' }, { type: 'null' }] },
457457
},
458458
]);
459459
expect(result.mutations).toEqual([
@@ -587,7 +587,7 @@ describe('ContractAnalyzer - Multi-File with Schema', () => {
587587
},
588588
required: ['id', 'name', 'age'],
589589
},
590-
{},
590+
{ type: 'null' },
591591
],
592592
},
593593
},
@@ -1076,4 +1076,154 @@ describe('ContractAnalyzer - Multi-File with Schema', () => {
10761076
},
10771077
]);
10781078
});
1079+
1080+
describe('null and undefined union types with schemas', () => {
1081+
it('should handle multi-file contract with null union schemas', () => {
1082+
const sourceFiles = {
1083+
'src/types.ts': `
1084+
export interface UserProfile {
1085+
id: string;
1086+
email: string | null;
1087+
verified: boolean;
1088+
}
1089+
`,
1090+
'src/contract.ts': `
1091+
import { UserProfile } from './types';
1092+
1093+
export default class NullableSchemaContract {
1094+
state = { profiles: [] as UserProfile[] };
1095+
1096+
getProfile(id: string | null): UserProfile | null {
1097+
return this.state.profiles[0];
1098+
}
1099+
1100+
createProfile(data: UserProfile | null): void {
1101+
if (data) {
1102+
this.state.profiles.push(data);
1103+
}
1104+
}
1105+
}
1106+
`,
1107+
};
1108+
1109+
const result = analyzer.analyzeMultiFileWithSchema(sourceFiles);
1110+
expect(result.queries).toEqual([
1111+
{
1112+
name: 'getProfile',
1113+
params: [
1114+
{
1115+
name: 'id',
1116+
schema: { anyOf: [{ type: 'string' }, { type: 'null' }] },
1117+
},
1118+
],
1119+
returnSchema: {
1120+
anyOf: [
1121+
{
1122+
type: 'object',
1123+
properties: {
1124+
id: { type: 'string' },
1125+
email: { anyOf: [{ type: 'string' }, { type: 'null' }] },
1126+
verified: { type: 'boolean' },
1127+
},
1128+
required: ['id', 'email', 'verified'],
1129+
},
1130+
{ type: 'null' },
1131+
],
1132+
},
1133+
},
1134+
]);
1135+
expect(result.mutations).toEqual([
1136+
{
1137+
name: 'createProfile',
1138+
params: [
1139+
{
1140+
name: 'data',
1141+
schema: {
1142+
anyOf: [
1143+
{
1144+
type: 'object',
1145+
properties: {
1146+
id: { type: 'string' },
1147+
email: { anyOf: [{ type: 'string' }, { type: 'null' }] },
1148+
verified: { type: 'boolean' },
1149+
},
1150+
required: ['id', 'email', 'verified'],
1151+
},
1152+
{ type: 'null' },
1153+
],
1154+
},
1155+
},
1156+
],
1157+
returnSchema: {},
1158+
},
1159+
]);
1160+
});
1161+
1162+
it('should handle multi-file contract with undefined union schemas', () => {
1163+
const sourceFiles = {
1164+
'src/config.ts': `
1165+
export type OptionalConfig = {
1166+
timeout?: number | undefined;
1167+
retries: number | null;
1168+
};
1169+
`,
1170+
'src/contract.ts': `
1171+
import { OptionalConfig } from './config';
1172+
1173+
export default class UndefinedSchemaContract {
1174+
state = { settings: {} };
1175+
1176+
updateConfig(config: OptionalConfig): OptionalConfig {
1177+
this.state.settings = { ...this.state.settings, ...config };
1178+
return config;
1179+
}
1180+
1181+
getTimeout(defaultValue: number | undefined): number {
1182+
return this.state.settings.timeout ?? defaultValue ?? 5000;
1183+
}
1184+
}
1185+
`,
1186+
};
1187+
1188+
const result = analyzer.analyzeMultiFileWithSchema(sourceFiles);
1189+
expect(result.queries).toEqual([
1190+
{
1191+
name: 'getTimeout',
1192+
params: [
1193+
{
1194+
name: 'defaultValue',
1195+
schema: { anyOf: [{ type: 'number' }, { type: 'null' }] },
1196+
},
1197+
],
1198+
returnSchema: { type: 'number' },
1199+
},
1200+
]);
1201+
expect(result.mutations).toEqual([
1202+
{
1203+
name: 'updateConfig',
1204+
params: [
1205+
{
1206+
name: 'config',
1207+
schema: {
1208+
type: 'object',
1209+
properties: {
1210+
timeout: { anyOf: [{ type: 'number' }, { type: 'null' }] },
1211+
retries: { anyOf: [{ type: 'number' }, { type: 'null' }] },
1212+
},
1213+
required: ['retries'],
1214+
},
1215+
},
1216+
],
1217+
returnSchema: {
1218+
type: 'object',
1219+
properties: {
1220+
timeout: { anyOf: [{ type: 'number' }, { type: 'null' }] },
1221+
retries: { anyOf: [{ type: 'number' }, { type: 'null' }] },
1222+
},
1223+
required: ['retries'],
1224+
},
1225+
},
1226+
]);
1227+
});
1228+
});
10791229
});

packages/parse/__tests__/ContractAnalyzer.singleFile.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,4 +666,72 @@ describe('ContractAnalyzer - Single File Analysis', () => {
666666
expect(result.mutations.map((m) => m.name)).toEqual(['addItem', 'processItem']);
667667
});
668668
});
669+
670+
describe('null and undefined union types', () => {
671+
it('should handle parameters with null union types', () => {
672+
const code = `
673+
export default class Contract {
674+
state: any;
675+
676+
add(num: number | null) {
677+
if (num !== null) {
678+
this.state.value += num;
679+
}
680+
}
681+
682+
getName(id: string | null): string {
683+
return id ? this.state.names[id] : 'unknown';
684+
}
685+
}
686+
`;
687+
688+
const result = analyzer.analyzeFromCode(code);
689+
expect(result.queries).toEqual([
690+
{
691+
name: 'getName',
692+
params: [{ name: 'id', type: 'string | null' }],
693+
returnType: 'string',
694+
},
695+
]);
696+
expect(result.mutations).toEqual([
697+
{
698+
name: 'add',
699+
params: [{ name: 'num', type: 'number | null' }],
700+
returnType: 'void',
701+
},
702+
]);
703+
});
704+
705+
it('should handle parameters with undefined union types and nullable return types', () => {
706+
const code = `
707+
export default class Contract {
708+
state: any;
709+
710+
updateCount(increment: number | undefined) {
711+
this.state.count += increment || 0;
712+
}
713+
714+
findUser(id: string): User | null | undefined {
715+
return this.state.users.find(u => u.id === id);
716+
}
717+
}
718+
`;
719+
720+
const result = analyzer.analyzeFromCode(code);
721+
expect(result.queries).toEqual([
722+
{
723+
name: 'findUser',
724+
params: [{ name: 'id', type: 'string' }],
725+
returnType: 'User | null | undefined',
726+
},
727+
]);
728+
expect(result.mutations).toEqual([
729+
{
730+
name: 'updateCount',
731+
params: [{ name: 'increment', type: 'number | undefined' }],
732+
returnType: 'void',
733+
},
734+
]);
735+
});
736+
});
669737
});

0 commit comments

Comments
 (0)