Skip to content

Commit c66ff6d

Browse files
Add x-glean-deprecated to Speakeasy deprecation format transformation
Transform x-glean-deprecated annotations in OpenAPI specs to Speakeasy- compatible deprecation format. This enables Speakeasy SDK generation to properly handle deprecated operations, parameters, and schema properties. The transformation: - Adds `deprecated: true` to elements with x-glean-deprecated - Generates `x-speakeasy-deprecation-message` with format: "Deprecated on {introduced}, removal scheduled for {removal}: {message}" - Preserves the original x-glean-deprecated annotation for internal tooling
1 parent 786f89b commit c66ff6d

File tree

2 files changed

+233
-0
lines changed

2 files changed

+233
-0
lines changed

src/source-spec-transformer.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,46 @@ export function transformEnumDescriptions(spec) {
256256
return spec;
257257
}
258258

259+
/**
260+
* Transforms x-glean-deprecated annotations to Speakeasy-compatible deprecation format
261+
* Adds deprecated: true and x-speakeasy-deprecation-message fields while preserving the original annotation
262+
* @param {Object} spec The OpenAPI spec object
263+
* @returns {Object} Transformed spec object
264+
*/
265+
export function transformGleanDeprecated(spec) {
266+
const processObject = (obj) => {
267+
if (!obj || typeof obj !== 'object') return;
268+
269+
// Process arrays
270+
if (Array.isArray(obj)) {
271+
obj.forEach(processObject);
272+
return;
273+
}
274+
275+
// Check if this object has x-glean-deprecated
276+
if (obj['x-glean-deprecated']) {
277+
const deprecation = obj['x-glean-deprecated'];
278+
279+
// Add deprecated: true
280+
obj.deprecated = true;
281+
282+
// Build the deprecation message with dates
283+
const message = `Deprecated on ${deprecation.introduced}, removal scheduled for ${deprecation.removal}${deprecation.message ? `: ${deprecation.message}` : ''}`;
284+
obj['x-speakeasy-deprecation-message'] = message;
285+
}
286+
287+
// Recursively process all properties
288+
Object.values(obj).forEach((value) => {
289+
if (value && typeof value === 'object') {
290+
processObject(value);
291+
}
292+
});
293+
};
294+
295+
processObject(spec);
296+
return spec;
297+
}
298+
259299
/**
260300
* Injects the open-api repository commit SHA into the spec info section
261301
* @param {Object} spec The OpenAPI spec object
@@ -334,6 +374,9 @@ export function transform(content, filename, commitSha) {
334374
// Apply x-enumDescriptions -> x-speakeasy-enum-descriptions transformation for all files
335375
transformEnumDescriptions(spec);
336376

377+
// Apply x-glean-deprecated -> Speakeasy deprecation format transformation for all files
378+
transformGleanDeprecated(spec);
379+
337380
// Apply admin duplicate operationId fix
338381
if (filename === 'admin_rest.yaml') {
339382
transformActAsBearerTokenToAPIToken(spec);

tests/source-spec-transformer.test.js

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
transformBearerAuthToAPIToken,
1010
transformServerVariables,
1111
transformEnumDescriptions,
12+
transformGleanDeprecated,
1213
injectOpenApiCommitSha,
1314
} from '../src/source-spec-transformer.js';
1415

@@ -423,4 +424,193 @@ describe('OpenAPI YAML Transformer', () => {
423424

424425
expect(transformedSpec.info['x-open-api-commit-sha']).toBeUndefined();
425426
});
427+
428+
test('transformGleanDeprecated adds Speakeasy deprecation fields to operations', () => {
429+
const testSpec = {
430+
paths: {
431+
'/test': {
432+
get: {
433+
operationId: 'getTest',
434+
'x-glean-deprecated': {
435+
id: 'uuid-123',
436+
message: 'Use /v2/test instead',
437+
introduced: '2024-01-15',
438+
removal: '2024-07-15',
439+
},
440+
},
441+
},
442+
},
443+
};
444+
445+
const transformedSpec = transformGleanDeprecated(testSpec);
446+
447+
expect(transformedSpec.paths['/test'].get.deprecated).toBe(true);
448+
expect(
449+
transformedSpec.paths['/test'].get['x-speakeasy-deprecation-message'],
450+
).toBe(
451+
'Deprecated on 2024-01-15, removal scheduled for 2024-07-15: Use /v2/test instead',
452+
);
453+
// Verify original annotation is preserved
454+
expect(
455+
transformedSpec.paths['/test'].get['x-glean-deprecated'],
456+
).toBeDefined();
457+
expect(transformedSpec.paths['/test'].get['x-glean-deprecated'].id).toBe(
458+
'uuid-123',
459+
);
460+
});
461+
462+
test('transformGleanDeprecated adds Speakeasy deprecation fields to parameters', () => {
463+
const testSpec = {
464+
paths: {
465+
'/test': {
466+
get: {
467+
parameters: [
468+
{
469+
name: 'oldParam',
470+
in: 'query',
471+
'x-glean-deprecated': {
472+
id: 'param-uuid',
473+
message: 'Use newParam instead',
474+
introduced: '2024-02-01',
475+
removal: '2024-08-01',
476+
},
477+
},
478+
],
479+
},
480+
},
481+
},
482+
};
483+
484+
const transformedSpec = transformGleanDeprecated(testSpec);
485+
486+
const param = transformedSpec.paths['/test'].get.parameters[0];
487+
expect(param.deprecated).toBe(true);
488+
expect(param['x-speakeasy-deprecation-message']).toBe(
489+
'Deprecated on 2024-02-01, removal scheduled for 2024-08-01: Use newParam instead',
490+
);
491+
expect(param['x-glean-deprecated']).toBeDefined();
492+
});
493+
494+
test('transformGleanDeprecated adds Speakeasy deprecation fields to schema properties', () => {
495+
const testSpec = {
496+
components: {
497+
schemas: {
498+
TestSchema: {
499+
type: 'object',
500+
properties: {
501+
oldField: {
502+
type: 'string',
503+
'x-glean-deprecated': {
504+
id: 'schema-uuid',
505+
message: 'Field renamed to newField',
506+
introduced: '2024-03-01',
507+
removal: '2024-09-01',
508+
docs: 'https://docs.example.com/migration',
509+
},
510+
},
511+
newField: {
512+
type: 'string',
513+
},
514+
},
515+
},
516+
},
517+
},
518+
};
519+
520+
const transformedSpec = transformGleanDeprecated(testSpec);
521+
522+
const oldField =
523+
transformedSpec.components.schemas.TestSchema.properties.oldField;
524+
expect(oldField.deprecated).toBe(true);
525+
expect(oldField['x-speakeasy-deprecation-message']).toBe(
526+
'Deprecated on 2024-03-01, removal scheduled for 2024-09-01: Field renamed to newField',
527+
);
528+
expect(oldField['x-glean-deprecated']).toBeDefined();
529+
expect(oldField['x-glean-deprecated'].docs).toBe(
530+
'https://docs.example.com/migration',
531+
);
532+
533+
// Verify non-deprecated field is unchanged
534+
const newField =
535+
transformedSpec.components.schemas.TestSchema.properties.newField;
536+
expect(newField.deprecated).toBeUndefined();
537+
expect(newField['x-speakeasy-deprecation-message']).toBeUndefined();
538+
});
539+
540+
test('transformGleanDeprecated handles specs without x-glean-deprecated', () => {
541+
const testSpec = {
542+
paths: {
543+
'/test': {
544+
get: {
545+
operationId: 'getTest',
546+
summary: 'Test endpoint',
547+
},
548+
},
549+
},
550+
components: {
551+
schemas: {
552+
TestSchema: {
553+
type: 'object',
554+
properties: {
555+
field: {
556+
type: 'string',
557+
},
558+
},
559+
},
560+
},
561+
},
562+
};
563+
564+
const transformedSpec = transformGleanDeprecated(testSpec);
565+
566+
// Verify no deprecated fields were added
567+
expect(transformedSpec.paths['/test'].get.deprecated).toBeUndefined();
568+
expect(
569+
transformedSpec.paths['/test'].get['x-speakeasy-deprecation-message'],
570+
).toBeUndefined();
571+
expect(
572+
transformedSpec.components.schemas.TestSchema.properties.field.deprecated,
573+
).toBeUndefined();
574+
});
575+
576+
test('transformGleanDeprecated handles nested deprecations', () => {
577+
const testSpec = {
578+
paths: {
579+
'/test': {
580+
post: {
581+
requestBody: {
582+
content: {
583+
'application/json': {
584+
schema: {
585+
properties: {
586+
deprecatedField: {
587+
type: 'string',
588+
'x-glean-deprecated': {
589+
id: 'nested-uuid',
590+
message: 'This field is deprecated',
591+
introduced: '2024-04-01',
592+
removal: '2024-10-01',
593+
},
594+
},
595+
},
596+
},
597+
},
598+
},
599+
},
600+
},
601+
},
602+
},
603+
};
604+
605+
const transformedSpec = transformGleanDeprecated(testSpec);
606+
607+
const deprecatedField =
608+
transformedSpec.paths['/test'].post.requestBody.content[
609+
'application/json'
610+
].schema.properties.deprecatedField;
611+
expect(deprecatedField.deprecated).toBe(true);
612+
expect(deprecatedField['x-speakeasy-deprecation-message']).toBe(
613+
'Deprecated on 2024-04-01, removal scheduled for 2024-10-01: This field is deprecated',
614+
);
615+
});
426616
});

0 commit comments

Comments
 (0)