Skip to content

Commit 0ea38eb

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 0ea38eb

2 files changed

Lines changed: 93 additions & 12 deletions

File tree

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

Lines changed: 92 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,14 @@ 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: { api: 'member', fqn: typeSpec.fqn, memberName: member.name },
737+
});
699738
this.code.line(
700739
`${this.rubyConstName(member.name)} = Jsii::Enum.new("${rubyDq(typeSpec.fqn)}", "${rubyDq(member.name)}")`,
701740
);
@@ -744,7 +783,9 @@ export class RubyGenerator extends Generator {
744783
? ' < Jsii::Struct'
745784
: '';
746785

747-
this.emitDocs(typeSpec);
786+
this.emitDocs(typeSpec, {
787+
apiLocation: { api: 'type', fqn: typeSpec.fqn },
788+
});
748789
this.code.open(`${kind} ${prefix}${rubyName}${baseString}`);
749790

750791
if (!typeSpec.datatype) {
@@ -796,6 +837,11 @@ export class RubyGenerator extends Generator {
796837
this.emitDocs(prop, {
797838
propertyType: prop.type,
798839
propertyOptional: prop.optional,
840+
apiLocation: {
841+
api: 'member',
842+
fqn: typeSpec.fqn,
843+
memberName: prop.name,
844+
},
799845
});
800846
this.code.line(`attr_reader :${this.rubyName(prop.name)}`);
801847
}
@@ -832,6 +878,11 @@ export class RubyGenerator extends Generator {
832878
this.emitDocs(prop, {
833879
propertyType: prop.type,
834880
propertyOptional: prop.optional,
881+
apiLocation: {
882+
api: 'member',
883+
fqn: typeSpec.fqn,
884+
memberName: prop.name,
885+
},
835886
});
836887
this.code.open(`def ${propRubyName}()`);
837888
this.code.line(`jsii_get_property("${rubyDq(prop.name)}")`);
@@ -868,6 +919,11 @@ export class RubyGenerator extends Generator {
868919
params: method.parameters,
869920
returns: method.spec?.returns,
870921
isMethod: true,
922+
apiLocation: {
923+
api: 'member',
924+
fqn: typeSpec.fqn,
925+
memberName: method.name,
926+
},
871927
});
872928
this.code.open(`def ${this.rubyName(method.name)}(${sigParams})`);
873929
for (const p of method.parameters) {
@@ -951,7 +1007,9 @@ export class RubyGenerator extends Generator {
9511007
(i) => `::${this.rubyFullTypeName(i)}`,
9521008
);
9531009

954-
this.emitDocs(typeSpec);
1010+
this.emitDocs(typeSpec, {
1011+
apiLocation: { api: 'type', fqn: typeSpec.fqn },
1012+
});
9551013
this.code.open(`class ${prefix}${rubyName} < ${baseClass}`);
9561014

9571015
for (const mixin of interfaceMixins) {
@@ -977,7 +1035,10 @@ export class RubyGenerator extends Generator {
9771035
})
9781036
.join(', ');
9791037

980-
this.emitDocs(initializer, { params: initializer.parameters });
1038+
this.emitDocs(initializer, {
1039+
params: initializer.parameters,
1040+
apiLocation: { api: 'initializer', fqn: typeSpec.fqn },
1041+
});
9811042
this.code.open(`def initialize(${initParams})`);
9821043
for (const p of initializer.parameters) {
9831044
const rubyParam = this.rubyName(p.name);
@@ -1084,6 +1145,11 @@ export class RubyGenerator extends Generator {
10841145
params: method.parameters,
10851146
returns: method.spec?.returns,
10861147
isMethod: true,
1148+
apiLocation: {
1149+
api: 'member',
1150+
fqn: typeSpec.fqn,
1151+
memberName: method.name,
1152+
},
10871153
});
10881154
this.code.open(`def self.${this.rubyMethodName(method)}(${sigParams})`);
10891155
for (const p of method.parameters) {
@@ -1112,6 +1178,11 @@ export class RubyGenerator extends Generator {
11121178
this.emitDocs(prop, {
11131179
propertyType: prop.type,
11141180
propertyOptional: prop.optional,
1181+
apiLocation: {
1182+
api: 'member',
1183+
fqn: typeSpec.fqn,
1184+
memberName: prop.name,
1185+
},
11151186
});
11161187
this.code.open(`def self.${rubyName}()`);
11171188
this.code.line(
@@ -1136,6 +1207,11 @@ export class RubyGenerator extends Generator {
11361207
this.emitDocs(prop, {
11371208
propertyType: prop.type,
11381209
propertyOptional: prop.optional,
1210+
apiLocation: {
1211+
api: 'member',
1212+
fqn: typeSpec.fqn,
1213+
memberName: prop.name,
1214+
},
11391215
});
11401216
this.code.open(`def ${rubyName}()`);
11411217
this.code.line(`jsii_get_property("${rubyDq(prop.name)}")`);
@@ -1178,6 +1254,11 @@ export class RubyGenerator extends Generator {
11781254
params: method.parameters,
11791255
returns: method.spec?.returns,
11801256
isMethod: true,
1257+
apiLocation: {
1258+
api: 'member',
1259+
fqn: typeSpec.fqn,
1260+
memberName: method.name,
1261+
},
11811262
});
11821263
this.code.open(`def ${this.rubyMethodName(method)}(${sigParams})`);
11831264
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)