Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Output cross-language names in generated docs #130

Merged
merged 8 commits into from
Aug 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/jsii-pacmak/bin/jsii-pacmak.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { SPEC_FILE_NAME } from '../node_modules/jsii-spec';
const argv = yargs
.usage('Usage: jsii-pacmak [-t target,...] [-o outdir] [package-dir]')
.option('targets', {
alias: 't',
alias: ['target', 't'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

type: 'array',
desc: 'target languages for which to generate bindings',
defaultDescription: 'all targets defined in `package.json` will be generated',
Expand Down Expand Up @@ -109,7 +109,7 @@ import { SPEC_FILE_NAME } from '../node_modules/jsii-spec';

// outdir is either by package.json/jsii.outdir (relative to package root) or via command line (relative to cwd)
const outDir = argv.outdir !== undefined ? path.resolve(process.cwd(), argv.outdir) : path.resolve(packageDir, pkg.jsii.outdir);
const targets = argv.targets || [ ...Object.keys(pkg.jsii.targets), 'js' ]; // "js" is an implicit target
const targets = argv.targets || [ ...Object.keys(pkg.jsii.targets), 'js' ]; // "js" is an implicit target.

logging.info(`Building ${pkg.name} (${targets.join(',')}) into ${path.relative(process.cwd(), outDir)}`);

Expand Down
61 changes: 61 additions & 0 deletions packages/jsii-pacmak/lib/target.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs = require('fs-extra');
import spec = require('jsii-spec');
import path = require('path');

import { IGenerator } from './generator';
Expand Down Expand Up @@ -48,6 +49,8 @@ export abstract class Target {
if (this.force || !await this.generator.upToDate(outDir)) {
await this.generator.generate(this.fingerprint);
await this.generator.save(outDir, tarball);
} else {
logging.info(`Generated code for ${this.targetName} was already up-to-date in ${outDir} (use --force to re-generate)`);
}
}

Expand Down Expand Up @@ -106,9 +109,67 @@ export abstract class Target {
}

export interface TargetConstructor {
/**
* Provides information about an assembly in the usual package repositories for the target. This includes information
* necessary to locate the package in the repositories (a URL to the repository's public endpoint), as well as usage
* instructions for the various configruation files (e.g: Maven POM, Gemfile, ...) and/or installation instructions
* using the standard command line tools (npm, yarn, ...).
*
* @param assm the assembly for which coodinates are requested.
*
* @return Information about the assembly in the various package managers supported for a given language. The return
* value is a hash, as some packages can be used across different languages (typescript & javascript, java &
* scala & clojure & kotlin...).
*/
toPackageInfos?: (assm: spec.Assembly) => { [language: string]: PackageInfo };

/**
* Provides the native way to reference a Type, for example a Java import statement, or a Javscript require directive.
* Particularly useful when generating documentation.
*
* @param type the JSII type for which a native reference is requested.
* @param options the target-specific options provided.
*
* @return the native reference for the target for each supported language (there can be multiple languages
* supported by a given target: typescript & javascript, java & scala & clojure & kotlin, ...)
*/
toNativeReference?: (type: spec.Type, options: any) => { [language: string]: string };

new(options: TargetOptions): Target;
}

/**
* Information about a package
*/
export interface PackageInfo {
/** The name by which the package repository is known */
repository: string;

/** The URL to the package within it's repository */
url: string;

/**
* Configuration fragments or installation instructions, by client scenario (e.g: maven + gradle). Values can be a
* plain string (documentation should render as a pre-formatted block of text using monospace font), or an object
* describing a language-tagged block of code.
*
* @example {
* maven: {
* language: 'xml',
* code: '<dependency><groupId>grp</groupId><artifactId>art</artifactId><version>version</version></dependency>'
* },
* gradle: "compile 'grp:art:version'",
* }
*
* @example {
* npm: { language: 'console', code: '$ npm install pkg' },
* yarn: { language: 'console', code: '$ yarn add pkg' },
* 'package.json': { language: json, code: '{"pkg": "^version" }' }
* }
*/
usage: { [label: string]: string | { language: string, code: string } };
}

export interface TargetOptions {
/** The name of the target language we are generating */
targetName: string;
Expand Down
37 changes: 35 additions & 2 deletions packages/jsii-pacmak/lib/targets/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,47 @@ import path = require('path');
import xmlbuilder = require('xmlbuilder');
import { Generator } from '../generator';
import logging = require('../logging');
import { Target, TargetOptions } from '../target';
import { PackageInfo, Target, TargetOptions } from '../target';
import { shell } from '../util';
import { VERSION, VERSION_DESC } from '../version';

// tslint:disable-next-line:no-var-requires
const spdxLicenseList = require('spdx-license-list');

export default class JavaPackageMaker extends Target {
export default class Java extends Target {
public static toPackageInfos(assm: spec.Assembly): { [language: string]: PackageInfo } {
const groupId = assm.targets!.java!.maven.groupId;
const artifactId = assm.targets!.java!.maven.artifactId;
const url = `https://repo1.maven.org/maven2/${groupId.replace(/\./g, '/')}/${artifactId}/${assm.version}/`;
return {
java: {
repository: 'Maven Central', url,
usage: {
'Apache Maven': {
language: 'xml',
code: xmlbuilder.create({
dependency: { groupId, artifactId, version: assm.version }
}).end({ pretty: true }).replace(/<\?\s*xml(\s[^>]+)?>\s*/m, '')
},
'Apache Buildr': `'${groupId}:${artifactId}:jar:${assm.version}'`,
'Apache Ivy': {
language: 'xml',
code: xmlbuilder.create({
dependency: { '@groupId': groupId, '@name': artifactId, '@rev': assm.version }
}).end({ pretty: true }).replace(/<\?\s*xml(\s[^>]+)?>\s*/m, '')
},
'Groovy Grape': `@Grapes(\n@Grab(group='${groupId}', module='${artifactId}', version='${assm.version}')\n)`,
'Gradle / Grails': `compile '${groupId}:${artifactId}:${assm.version}'`,
}
}
};
}

public static toNativeReference(type: spec.Type, options: any) {
const [, ...name] = type.fqn.split('.');
return { java: `import ${[options.package, ...name].join('.')};` };
}

protected readonly generator = new JavaGenerator();

constructor(options: TargetOptions) {
Expand Down
38 changes: 37 additions & 1 deletion packages/jsii-pacmak/lib/targets/js.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,44 @@
import * as spec from 'jsii-spec';
import { Generator } from '../generator';
import { Target, TargetOptions } from '../target';
import { PackageInfo, Target, TargetOptions } from '../target';

export default class JavaScript extends Target {
public static toPackageInfos(assm: spec.Assembly): { [language: string]: PackageInfo } {
const packageInfo: PackageInfo = {
repository: 'NPM',
url: `https://www.npmjs.com/package/${assm.name}/v/${assm.version}`,
usage: {
'package.json': {
language: 'js',
code: JSON.stringify({ [assm.name]: `^${assm.version}` }, null, 2)
},
'npm': {
language: 'console',
code: `$ npm i ${assm.name}@${assm.version}`
},
'yarn': {
language: 'console',
code: `$ yarn add ${assm.name}@${assm.version}`
}
}
};
return { typescript: packageInfo, javascript: packageInfo };
}

public static toNativeReference(type: spec.Type) {
const [, ...name] = type.fqn.split('.');
const resolvedName = name.join('.');
const result: { typescript: string, javascript?: string } = {
typescript: `import { ${resolvedName} } from '${type.assembly}';`
};
if (!spec.isInterfaceType(type)) {
result.javascript = `const { ${resolvedName} } = require('${type.assembly}');`;
} else {
result.javascript = `// ${resolvedName} is an interface`;
}
return result;
}

protected readonly generator = new PackOnly();

constructor(options: TargetOptions) {
Expand Down
104 changes: 99 additions & 5 deletions packages/jsii-pacmak/lib/targets/sphinx.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as fs from 'fs-extra';
import * as spec from 'jsii-spec';
import * as path from 'path';
import fs = require('fs-extra');
import spec = require('jsii-spec');
import path = require('path');
import { Generator } from '../generator';
import { Target, TargetOptions } from '../target';
import { Target, TargetConstructor, TargetOptions } from '../target';

export default class Sphinx extends Target {
protected readonly generator = new SphinxDocsGenerator();
Expand Down Expand Up @@ -31,6 +31,7 @@ class SphinxDocsGenerator extends Generator {
private readmeFile?: string;
private namespaceStack = new Array<NamespaceStackEntry>();
private tocPath = new Array<string>();
private targets: { [name: string]: TargetConstructor } = {};

private get topNamespace(): NamespaceStackEntry {
return this.namespaceStack.length > 0
Expand All @@ -46,6 +47,11 @@ class SphinxDocsGenerator extends Generator {
this.code.indentation = 3;
}

public async load(packageRoot: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we shouldn't just include set of target constructors as part of load. It is available in the outer scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can couple everything to everything else. Doesn't mean it's a good idea...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep it like this for now until the 2nd case where a generator will need this info

await super.load(packageRoot);
this.targets = await Target.findAll();
}

public async upToDate(outDir: string): Promise<boolean> {
const mainFile = path.join(outDir, `${fsSafeName(this.assembly.name)}.rst`);
try {
Expand Down Expand Up @@ -85,6 +91,43 @@ class SphinxDocsGenerator extends Generator {

this.openSection(assm.name);
this.code.line();
if (assm.targets) {
this.code.openBlock('.. tabs::');
this.code.line();
for (const targetName of Object.keys(assm.targets).sort()) {
const target = this.targets[targetName];
if (!target || !target.toPackageInfos) { continue; }
const packageInfos = target.toPackageInfos(assm);
for (const language of Object.keys(packageInfos).sort()) {
const packageInfo = packageInfos[language];
this.code.openBlock(`.. group-tab:: ${formatLanguage(language)}`);
this.code.line();

this.code.line(`View in \`${packageInfo.repository} <${packageInfo.url}>\`_`);
this.code.line();

for (const mgrName of Object.keys(packageInfo.usage).sort()) {
const mgr = packageInfo.usage[mgrName];
this.code.line(`**${mgrName}**:`);
this.code.line();
if (typeof mgr === 'string') {
this.code.openBlock('.. code-block:: none');
this.code.line();
mgr.split('\n').forEach(s => this.code.line(s));
this.code.closeBlock();
} else {
this.code.openBlock(`.. code-block:: ${mgr.language}`);
this.code.line();
mgr.code.split('\n').forEach(s => this.code.line(s));
this.code.closeBlock();
}
}

this.code.closeBlock();
}
}
this.code.closeBlock();
}

this.assemblyName = assm.name;
}
Expand Down Expand Up @@ -170,6 +213,7 @@ class SphinxDocsGenerator extends Generator {
}

this.code.openBlock(`.. py:class:: ${className}${sig}`);
this.renderNames(cls);
this.renderDocsLine(cls);
this.code.line();

Expand Down Expand Up @@ -267,6 +311,8 @@ class SphinxDocsGenerator extends Generator {
}

this.code.openBlock(`.. py:class:: ${enumName}`);
this.renderNames(enm);
this.renderDocsLine(enm);
this.code.line();
}

Expand All @@ -288,7 +334,7 @@ class SphinxDocsGenerator extends Generator {
}

this.code.openBlock(`.. py:class:: ${ifc.name}`);

this.renderNames(ifc);
this.renderDocsLine(ifc);
this.code.line();

Expand Down Expand Up @@ -516,6 +562,32 @@ class SphinxDocsGenerator extends Generator {
private toNativeFqn(name: string): string {
return name;
}

private async renderNames(type: spec.Type) {
this.code.line();
this.code.line('**Language-specific names:**');
this.code.line();
this.code.openBlock('.. tabs::');
this.code.line();

if (this.assembly.targets) {
for (const targetName of Object.keys(this.assembly.targets).sort()) {
if (targetName === 'sphinx') { continue; }
const target = this.targets[targetName];
if (!target || !target.toNativeReference) { continue; }
const options = this.assembly.targets[targetName];

const names = target.toNativeReference(type, options);
for (const language of Object.keys(names).sort()) {
this.code.openBlock(`.. code-tab:: ${language}`);
this.code.line();
this.code.line(names[language]);
this.code.closeBlock();
}
}
}
this.code.closeBlock();
}
}

function dup(char: string, times: number) {
Expand All @@ -533,3 +605,25 @@ function fsSafeName(x: string) {
// Strip unsafe characters
return x.replace(/[^a-zA-Z0-9_.-]/g, '_');
}

/**
* Obtains a display-friendly string from a language name.
*
* @param language the language name code (e.g: javascript)
*
* @returns a display-friendly name if possible (e.g: JavaScript)
*/
function formatLanguage(language: string): string {
switch (language) {
case 'csharp':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't seen the .NET target changes. Should we at least emit a C# option there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not publishing anything to Nuget yet, I figured it was probably best if our documentation doesn't start pointing people at places that don't exist...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#149 created to track the C# fragment.

return 'C#';
case 'java':
return 'Java';
case 'javascript':
return 'JavaScript';
case 'typescript':
return 'TypeScript';
default:
return language;
}
}
5 changes: 5 additions & 0 deletions packages/jsii-pacmak/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading