Skip to content

Commit d9bdd9e

Browse files
authored
Merge pull request #351 from abraham/copilot/fix-350
Fix missing date formats for Date and DateTime type fields
2 parents 7d52dbf + 9085409 commit d9bdd9e

File tree

6 files changed

+182
-3
lines changed

6 files changed

+182
-3
lines changed

dist/schema.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,8 @@
192192
},
193193
"date_of_birth": {
194194
"type": "string",
195-
"description": "String ([Date]), required if the server has a minimum age requirement."
195+
"description": "String ([Date]), required if the server has a minimum age requirement.",
196+
"format": "date"
196197
},
197198
"reason": {
198199
"type": "string",
@@ -22150,7 +22151,8 @@
2215022151
"properties": {
2215122152
"scheduled_at": {
2215222153
"type": "string",
22153-
"description": "[Datetime] at which the status will be published. Must be at least 5 minutes into the future."
22154+
"description": "[Datetime] at which the status will be published. Must be at least 5 minutes into the future.",
22155+
"format": "date-time"
2215422156
}
2215522157
}
2215622158
}
@@ -37872,7 +37874,8 @@
3787237874
},
3787337875
"scheduled_at": {
3787437876
"type": "string",
37875-
"description": "[Datetime] at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future."
37877+
"description": "[Datetime] at which to schedule a status. Providing this parameter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future.",
37878+
"format": "date-time"
3787637879
},
3787737880
"sensitive": {
3787837881
"type": "boolean",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { EntityConverter } from '../../generators/EntityConverter';
2+
import { TypeParser } from '../../generators/TypeParser';
3+
import { UtilityHelpers } from '../../generators/UtilityHelpers';
4+
import { EntityClass } from '../../interfaces/EntityClass';
5+
import { OpenAPISpec } from '../../interfaces/OpenAPISchema';
6+
7+
describe('EntityConverter - Nullable Date Format Handling', () => {
8+
let entityConverter: EntityConverter;
9+
let typeParser: TypeParser;
10+
let utilityHelpers: UtilityHelpers;
11+
12+
beforeEach(() => {
13+
utilityHelpers = new UtilityHelpers();
14+
typeParser = new TypeParser(utilityHelpers);
15+
entityConverter = new EntityConverter(typeParser, utilityHelpers);
16+
});
17+
18+
it('should preserve date format for nullable date fields', () => {
19+
const entities: EntityClass[] = [
20+
{
21+
name: 'TestEntity',
22+
description: 'Test entity with nullable date fields',
23+
attributes: [
24+
{
25+
name: 'last_status_at',
26+
type: 'String ([Date](/api/datetime-format#date))',
27+
description: 'When the most recent status was posted.',
28+
optional: false,
29+
nullable: true,
30+
deprecated: false,
31+
enumValues: [],
32+
versions: ['1.0.0'],
33+
},
34+
{
35+
name: 'regular_nullable_string',
36+
type: 'String',
37+
description: 'A regular nullable string',
38+
optional: false,
39+
nullable: true,
40+
deprecated: false,
41+
enumValues: [],
42+
versions: ['1.0.0'],
43+
},
44+
],
45+
},
46+
];
47+
48+
const spec: OpenAPISpec = {
49+
openapi: '3.0.0',
50+
info: { title: 'Test', version: '1.0.0' },
51+
paths: {},
52+
};
53+
54+
entityConverter.convertEntities(entities, spec);
55+
56+
const schema = spec.components?.schemas?.['TestEntity'];
57+
expect(schema).toBeDefined();
58+
expect(schema?.properties).toBeDefined();
59+
60+
// Nullable date field should have date format preserved
61+
const lastStatusAtProperty = schema?.properties?.['last_status_at'];
62+
expect(lastStatusAtProperty?.type).toEqual(['string', 'null']);
63+
expect(lastStatusAtProperty?.format).toBe('date');
64+
65+
// Regular nullable string should NOT have format
66+
const regularNullableProperty =
67+
schema?.properties?.['regular_nullable_string'];
68+
expect(regularNullableProperty?.type).toEqual(['string', 'null']);
69+
expect(regularNullableProperty?.format).toBeUndefined();
70+
});
71+
72+
it('should preserve datetime format for nullable datetime fields', () => {
73+
const entities: EntityClass[] = [
74+
{
75+
name: 'TestEntity',
76+
description: 'Test entity with nullable datetime fields',
77+
attributes: [
78+
{
79+
name: 'created_at',
80+
type: 'String ([Datetime](/api/datetime-format#datetime))',
81+
description: 'When the entity was created.',
82+
optional: false,
83+
nullable: true,
84+
deprecated: false,
85+
enumValues: [],
86+
versions: ['1.0.0'],
87+
},
88+
],
89+
},
90+
];
91+
92+
const spec: OpenAPISpec = {
93+
openapi: '3.0.0',
94+
info: { title: 'Test', version: '1.0.0' },
95+
paths: {},
96+
};
97+
98+
entityConverter.convertEntities(entities, spec);
99+
100+
const schema = spec.components?.schemas?.['TestEntity'];
101+
expect(schema).toBeDefined();
102+
103+
// Nullable datetime field should have date-time format preserved
104+
const createdAtProperty = schema?.properties?.['created_at'];
105+
expect(createdAtProperty?.type).toEqual(['string', 'null']);
106+
expect(createdAtProperty?.format).toBe('date-time');
107+
});
108+
109+
it('should preserve email format for nullable email fields', () => {
110+
const entities: EntityClass[] = [
111+
{
112+
name: 'TestEntity',
113+
description: 'Test entity with nullable email fields',
114+
attributes: [
115+
{
116+
name: 'email',
117+
type: 'String',
118+
description: 'The email address of the user.',
119+
optional: false,
120+
nullable: true,
121+
deprecated: false,
122+
enumValues: [],
123+
versions: ['1.0.0'],
124+
},
125+
],
126+
},
127+
];
128+
129+
const spec: OpenAPISpec = {
130+
openapi: '3.0.0',
131+
info: { title: 'Test', version: '1.0.0' },
132+
paths: {},
133+
};
134+
135+
entityConverter.convertEntities(entities, spec);
136+
137+
const schema = spec.components?.schemas?.['TestEntity'];
138+
expect(schema).toBeDefined();
139+
140+
// Nullable email field should have email format preserved
141+
const emailProperty = schema?.properties?.['email'];
142+
expect(emailProperty?.type).toEqual(['string', 'null']);
143+
expect(emailProperty?.format).toBe('email');
144+
});
145+
});

src/generators/EntityConverter.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,8 @@ class EntityConverter {
467467
} else if (property.type && typeof property.type === 'string') {
468468
// For regular type properties, convert type to an array that includes null
469469
property.type = [property.type, 'null'];
470+
// Preserve format property for nullable fields
471+
// Note: format should still apply to the non-null value
470472
}
471473
}
472474

src/generators/TypeParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,11 @@ class TypeParser {
347347
: undefined,
348348
};
349349

350+
// Add format if available
351+
if (param.schema.format) {
352+
schema.format = param.schema.format;
353+
}
354+
350355
// Add enum values if available - for arrays, put enum on items instead of array
351356
if (param.enumValues && param.enumValues.length > 0) {
352357
if (param.schema.type === 'array') {

src/interfaces/ApiParameter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ interface ApiParameter {
1919
items?: ApiProperty;
2020
properties?: Record<string, ApiProperty>;
2121
enum?: string[];
22+
format?: string;
2223
};
2324
}
2425

src/parsers/ParameterParser.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,29 @@ export class ParameterParser {
499499
| 'integer',
500500
};
501501

502+
// For string parameters, check for date/datetime formats
503+
if (param.schema.type === 'string' && rawParam.description) {
504+
const description = rawParam.description;
505+
506+
// Check for date/datetime formats
507+
if (
508+
description.includes('[Date]') &&
509+
!description.toLowerCase().includes('[datetime]') &&
510+
!description.toLowerCase().includes('[iso8601') &&
511+
!description.toLowerCase().includes('iso8601')
512+
) {
513+
param.schema.format = 'date';
514+
} else if (
515+
description.includes('[Datetime]') ||
516+
description.includes('[ISO8601') ||
517+
description.toLowerCase().includes('iso8601') ||
518+
(description.toLowerCase().includes('datetime') &&
519+
!description.toLowerCase().includes('datetime-format'))
520+
) {
521+
param.schema.format = 'date-time';
522+
}
523+
}
524+
502525
parameters.push(param);
503526
}
504527
}

0 commit comments

Comments
 (0)