Skip to content

Commit d2c88dd

Browse files
omarqureshiclaude
andcommitted
feat(ruby): translate @example snippets to Ruby via Rosetta
The Ruby pacmak target emitted jsii @example docs verbatim, leaving TypeScript snippets in generated Ruby docstrings. Wire in RosettaTabletReader and translate each example to Ruby (TargetLanguage.RUBY), mirroring the Python target. Examples degrade to the original source if translation fails. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f8a73de commit d2c88dd

2 files changed

Lines changed: 97 additions & 12 deletions

File tree

packages/jsii-pacmak/lib/targets/ruby.ts

Lines changed: 96 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ import * as spec from '@jsii/spec';
22
import { toSnakeCase, toPascalCase } from 'codemaker';
33
import * as fs from 'fs-extra';
44
import * as reflect from 'jsii-reflect';
5+
import {
6+
ApiLocation,
7+
enforcesStrictMode,
8+
RosettaTabletReader,
9+
TargetLanguage,
10+
} from 'jsii-rosetta';
511
import * as path from 'path';
612

713
import { Generator, Legalese } from '../generator';
14+
import { assertSpecIsRosettaCompatible } from '../rosetta-assembly';
815
import { Target, TargetOptions } from '../target';
916
import { subprocess } from '../util';
1017
import { VERSION } from '../version';
@@ -17,7 +24,7 @@ export class RubyTarget extends Target {
1724

1825
public constructor(options: TargetOptions) {
1926
super(options);
20-
this.generator = new RubyGenerator(options);
27+
this.generator = new RubyGenerator(options.rosetta, options);
2128
}
2229

2330
public async build(sourceDir: string, outDir: string): Promise<void> {
@@ -211,12 +218,32 @@ const RUBY_RESERVED_NAMES = new Set([
211218
]);
212219

213220
export class RubyGenerator extends Generator {
214-
public constructor(options: TargetOptions) {
221+
public constructor(
222+
private readonly rosetta: RosettaTabletReader,
223+
options: TargetOptions,
224+
) {
215225
super({ runtimeTypeChecking: options.runtimeTypeChecking });
216226
// Ruby convention is 2-space indentation (CodeMaker defaults to 4).
217227
this.code.indentation = 2;
218228
}
219229

230+
/**
231+
* Translate a jsii `@example` (authored in TypeScript) into idiomatic Ruby
232+
* via Rosetta. Falls back to the original text if translation fails, so a
233+
* bad snippet degrades to a TypeScript example rather than breaking the
234+
* build.
235+
*/
236+
private convertExample(example: string, apiLocation: ApiLocation): string {
237+
assertSpecIsRosettaCompatible(this.assembly);
238+
const translated = this.rosetta.translateExample(
239+
apiLocation,
240+
example,
241+
TargetLanguage.RUBY,
242+
enforcesStrictMode(this.assembly),
243+
);
244+
return translated.source;
245+
}
246+
220247
/**
221248
* Normalize a type reference to its raw `spec.TypeReference` shape.
222249
* Call sites hold two shapes: jsii-reflect `TypeReference` instances
@@ -389,6 +416,12 @@ export class RubyGenerator extends Generator {
389416
/** Property getter: emit an @return of the property's type. */
390417
propertyType?: RubyTypeRef;
391418
propertyOptional?: boolean;
419+
/**
420+
* The API location the docs belong to. When present, `@example`
421+
* snippets are translated to Ruby via Rosetta; without it they are
422+
* emitted verbatim (i.e. as the original TypeScript).
423+
*/
424+
apiLocation?: ApiLocation;
392425
} = {},
393426
): void {
394427
const docs: spec.Docs = this.rawDocs(docsSource) ?? {};
@@ -430,12 +463,14 @@ export class RubyGenerator extends Generator {
430463
tags.push(`# @see ${this.inlineDoc(docs.see)}`);
431464
}
432465

433-
const exampleLines = docs.example
466+
const exampleText =
467+
docs.example && opts.apiLocation
468+
? this.convertExample(docs.example, opts.apiLocation)
469+
: docs.example;
470+
const exampleLines = exampleText
434471
? [
435472
'# @example',
436-
...docs.example
437-
.split('\n')
438-
.map((l) => `# ${l.trimEnd()}`.trimEnd()),
473+
...exampleText.split('\n').map((l) => `# ${l.trimEnd()}`.trimEnd()),
439474
]
440475
: [];
441476

@@ -692,10 +727,18 @@ export class RubyGenerator extends Generator {
692727
(m) => this.rubyConstName(m.name),
693728
typeSpec.fqn,
694729
);
695-
this.emitDocs(typeSpec);
730+
this.emitDocs(typeSpec, {
731+
apiLocation: { api: 'type', fqn: typeSpec.fqn },
732+
});
696733
this.code.open(`module ${prefix}${this.rubyModuleName(typeSpec.name)}`);
697734
for (const member of resolvedMembers) {
698-
this.emitDocs(member);
735+
this.emitDocs(member, {
736+
apiLocation: {
737+
api: 'member',
738+
fqn: typeSpec.fqn,
739+
memberName: member.name,
740+
},
741+
});
699742
this.code.line(
700743
`${this.rubyConstName(member.name)} = Jsii::Enum.new("${rubyDq(typeSpec.fqn)}", "${rubyDq(member.name)}")`,
701744
);
@@ -744,7 +787,9 @@ export class RubyGenerator extends Generator {
744787
? ' < Jsii::Struct'
745788
: '';
746789

747-
this.emitDocs(typeSpec);
790+
this.emitDocs(typeSpec, {
791+
apiLocation: { api: 'type', fqn: typeSpec.fqn },
792+
});
748793
this.code.open(`${kind} ${prefix}${rubyName}${baseString}`);
749794

750795
if (!typeSpec.datatype) {
@@ -796,6 +841,11 @@ export class RubyGenerator extends Generator {
796841
this.emitDocs(prop, {
797842
propertyType: prop.type,
798843
propertyOptional: prop.optional,
844+
apiLocation: {
845+
api: 'member',
846+
fqn: typeSpec.fqn,
847+
memberName: prop.name,
848+
},
799849
});
800850
this.code.line(`attr_reader :${this.rubyName(prop.name)}`);
801851
}
@@ -832,6 +882,11 @@ export class RubyGenerator extends Generator {
832882
this.emitDocs(prop, {
833883
propertyType: prop.type,
834884
propertyOptional: prop.optional,
885+
apiLocation: {
886+
api: 'member',
887+
fqn: typeSpec.fqn,
888+
memberName: prop.name,
889+
},
835890
});
836891
this.code.open(`def ${propRubyName}()`);
837892
this.code.line(`jsii_get_property("${rubyDq(prop.name)}")`);
@@ -868,6 +923,11 @@ export class RubyGenerator extends Generator {
868923
params: method.parameters,
869924
returns: method.spec?.returns,
870925
isMethod: true,
926+
apiLocation: {
927+
api: 'member',
928+
fqn: typeSpec.fqn,
929+
memberName: method.name,
930+
},
871931
});
872932
this.code.open(`def ${this.rubyName(method.name)}(${sigParams})`);
873933
for (const p of method.parameters) {
@@ -951,7 +1011,9 @@ export class RubyGenerator extends Generator {
9511011
(i) => `::${this.rubyFullTypeName(i)}`,
9521012
);
9531013

954-
this.emitDocs(typeSpec);
1014+
this.emitDocs(typeSpec, {
1015+
apiLocation: { api: 'type', fqn: typeSpec.fqn },
1016+
});
9551017
this.code.open(`class ${prefix}${rubyName} < ${baseClass}`);
9561018

9571019
for (const mixin of interfaceMixins) {
@@ -977,7 +1039,10 @@ export class RubyGenerator extends Generator {
9771039
})
9781040
.join(', ');
9791041

980-
this.emitDocs(initializer, { params: initializer.parameters });
1042+
this.emitDocs(initializer, {
1043+
params: initializer.parameters,
1044+
apiLocation: { api: 'initializer', fqn: typeSpec.fqn },
1045+
});
9811046
this.code.open(`def initialize(${initParams})`);
9821047
for (const p of initializer.parameters) {
9831048
const rubyParam = this.rubyName(p.name);
@@ -1084,6 +1149,11 @@ export class RubyGenerator extends Generator {
10841149
params: method.parameters,
10851150
returns: method.spec?.returns,
10861151
isMethod: true,
1152+
apiLocation: {
1153+
api: 'member',
1154+
fqn: typeSpec.fqn,
1155+
memberName: method.name,
1156+
},
10871157
});
10881158
this.code.open(`def self.${this.rubyMethodName(method)}(${sigParams})`);
10891159
for (const p of method.parameters) {
@@ -1112,6 +1182,11 @@ export class RubyGenerator extends Generator {
11121182
this.emitDocs(prop, {
11131183
propertyType: prop.type,
11141184
propertyOptional: prop.optional,
1185+
apiLocation: {
1186+
api: 'member',
1187+
fqn: typeSpec.fqn,
1188+
memberName: prop.name,
1189+
},
11151190
});
11161191
this.code.open(`def self.${rubyName}()`);
11171192
this.code.line(
@@ -1136,6 +1211,11 @@ export class RubyGenerator extends Generator {
11361211
this.emitDocs(prop, {
11371212
propertyType: prop.type,
11381213
propertyOptional: prop.optional,
1214+
apiLocation: {
1215+
api: 'member',
1216+
fqn: typeSpec.fqn,
1217+
memberName: prop.name,
1218+
},
11391219
});
11401220
this.code.open(`def ${rubyName}()`);
11411221
this.code.line(`jsii_get_property("${rubyDq(prop.name)}")`);
@@ -1178,6 +1258,11 @@ export class RubyGenerator extends Generator {
11781258
params: method.parameters,
11791259
returns: method.spec?.returns,
11801260
isMethod: true,
1261+
apiLocation: {
1262+
api: 'member',
1263+
fqn: typeSpec.fqn,
1264+
memberName: method.name,
1265+
},
11811266
});
11821267
this.code.open(`def ${this.rubyMethodName(method)}(${sigParams})`);
11831268
for (const p of method.parameters) {

packages/jsii-pacmak/test/targets/ruby/ruby-names.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe('Ruby naming behavior', () => {
3535
},
3636
};
3737

38-
rubyTarget = new RubyGenerator({
38+
rubyTarget = new RubyGenerator({} as any, {
3939
targetName: 'ruby',
4040
packageDir: '.',
4141
assembly,

0 commit comments

Comments
 (0)