Skip to content
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
103 changes: 103 additions & 0 deletions src/code-sample-transformer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,33 @@ export function* codeSnippets(pathSpec) {
}
}

/**
* Generator that yields language and code sample pairs from pathSpec, but only
* for operations that are marked as experimental via `x-glean-experimental`.
*
* Experimental endpoints are gated behind an opt-in flag in the SDKs (surfaced
* as the `X-Glean-Include-Experimental` request header). Without it, the SDK
* will refuse to call the endpoint, so the generated samples for these
* operations need the flag to actually run.
*
* @param {Object} pathSpec The OpenAPI spec object for the specific path being processed
* @yields {[string, Object]} A tuple containing the language and code sample object
*/
export function* experimentalCodeSnippets(pathSpec) {
for (const [_, methodSpec] of Object.entries(pathSpec)) {
if (
methodSpec &&
methodSpec['x-glean-experimental'] &&
methodSpec['x-codeSamples']
) {
const codeSamples = methodSpec['x-codeSamples'];
for (const sample of codeSamples) {
yield [sample.lang, sample];
}
}
}
}

/**
* Extracts code snippet for a specific language from pathSpec
* @param {Object} pathSpec The OpenAPI spec object for the specific path being processed
Expand Down Expand Up @@ -139,6 +166,81 @@ export function addServerURLToCodeSamples(spec) {
return spec;
}

/**
* Transforms code samples for experimental endpoints (those marked with
* `x-glean-experimental`) so they include the SDK option needed to opt into
* experimental features. Without this option the SDKs will not call the
* endpoint, so the samples would not actually run as-is.
*
* This must run *after* {@link addServerURLToCodeSamples} so that the
* server URL constructor line already exists to anchor the injection onto
* (for Python/TypeScript/Go).
*
* Supported transformations:
* - Python: Adds `include_experimental=True,` after the `server_url` line
* - TypeScript: Adds `includeExperimental: true,` after the `serverURL` line
* - Go: Adds `apiclientgo.WithIncludeExperimental(true),` after `WithServerURL`
* - Java: Swaps `Glean.builder()` for the regen-safe `GleanBuilder.create()`
* (adding its import) and adds `.includeExperimental(true)` after `.apiToken(...)`
*
* @param {Object} spec The OpenAPI specification object containing code samples
* @returns {Object} The modified OpenAPI specification with updated code samples
*/
export function addIncludeExperimentalToCodeSamples(spec) {
const transformationsByLang = {
python: [
[
/([\s]*)(server_url="https:\/\/mycompany-be\.glean\.com",)/,
'$1$2$1include_experimental=True,',
],
],
typescript: [
[
/([\s]*)(serverURL: "https:\/\/mycompany-be\.glean\.com",)/,
'$1$2$1includeExperimental: true,',
],
],
go: [
[
/([\s]*)(apiclientgo\.WithServerURL\("https:\/\/mycompany-be\.glean\.com"\),)/,
'$1$2$1apiclientgo.WithIncludeExperimental(true),',
],
],
java: [
// The experimental flag lives on the regen-safe GleanBuilder, so add its
// import alongside the existing Glean import.
[
/(import com\.glean\.api_client\.glean_api_client\.Glean;\n)/,
'$1import com.glean.api_client.glean_api_client.hooks.GleanBuilder;\n',
],
// Swap the generated builder for the regen-safe one that exposes the flag.
[/Glean\.builder\(\)/, 'GleanBuilder.create()'],
// Add the flag right after the apiToken(...) call, matching indentation.
[/([\s]*)(\.apiToken\([^\n]*\))/, '$1$2$1.includeExperimental(true)'],
],
};

for (const [_, pathSpec] of path(spec)) {
for (const [lang, codeSample] of experimentalCodeSnippets(pathSpec)) {
const transformations = transformationsByLang[lang];

if (!transformations) {
continue;
}

let codeSampleSource = codeSample.source;

for (const [pattern, replacement] of transformations) {
codeSampleSource = codeSampleSource.replace(pattern, replacement);
}

codeSample.source = codeSampleSource;
}
}

return spec;
}

/**
* Transforms OpenAPI YAML by adjusting server URLs and paths
* @param {string} content The OpenAPI YAML content
Expand All @@ -150,6 +252,7 @@ export function transform(content, _filename) {

transformPythonCodeSamplesToPython(spec);
addServerURLToCodeSamples(spec);
addIncludeExperimentalToCodeSamples(spec);

return yaml.dump(spec, {
lineWidth: -1, // Preserve line breaks
Expand Down
157 changes: 157 additions & 0 deletions tests/code-sample-transformer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,4 +286,161 @@ paths:
`);
});
});

describe('experimentalCodeSnippets', () => {
test('yields samples only for operations marked x-glean-experimental', () => {
const spec = yaml.load(
readFixture('example-with-experimental-code-samples.yaml'),
);

const experimentalLangs = [];
for (const [, pathSpec] of codeSampleTransformer.path(spec)) {
for (const [lang] of codeSampleTransformer.experimentalCodeSnippets(
pathSpec,
)) {
experimentalLangs.push(lang);
}
}

// Only the /api/agents/search (experimental) samples should be yielded,
// not the /api/agents/stable ones.
expect(experimentalLangs).toMatchInlineSnapshot(`
[
"python",
"typescript",
"go",
"java",
]
`);
});
});

describe('addIncludeExperimentalToCodeSamples', () => {
let spec;

beforeEach(() => {
spec = yaml.load(
readFixture('example-with-experimental-code-samples.yaml'),
);
// Mirror the production order: server URL is injected first, then the
// experimental flag is anchored onto it.
codeSampleTransformer.addServerURLToCodeSamples(spec);
codeSampleTransformer.addIncludeExperimentalToCodeSamples(spec);
});

test('adds include_experimental to the experimental Python sample', () => {
const experimentalSpec = spec.paths['/api/agents/search'];

expect(
codeSampleTransformer.extractCodeSnippet(experimentalSpec, 'python')
.source,
).toMatchInlineSnapshot(`
"from glean.api_client import Glean
import os


with Glean(
api_token=os.getenv("GLEAN_API_TOKEN", ""),
server_url="https://mycompany-be.glean.com",
include_experimental=True,
) as glean:

res = glean.agents.search(name="HR Policy Agent")

# Handle response
print(res)"
`);
});

test('adds includeExperimental to the experimental TypeScript sample', () => {
const experimentalSpec = spec.paths['/api/agents/search'];

expect(
codeSampleTransformer.extractCodeSnippet(experimentalSpec, 'typescript')
.source,
).toMatchInlineSnapshot(`
"import { Glean } from "@gleanwork/api-client";

const glean = new Glean({
apiToken: process.env["GLEAN_API_TOKEN"] ?? "",
serverURL: "https://mycompany-be.glean.com",
includeExperimental: true,
});

async function run() {
const result = await glean.agents.search({
name: "HR Policy Agent",
});

console.log(result);
}

run();"
`);
});

test('adds WithIncludeExperimental to the experimental Go sample', () => {
const experimentalSpec = spec.paths['/api/agents/search'];

expect(
codeSampleTransformer.extractCodeSnippet(experimentalSpec, 'go').source,
).toContain('apiclientgo.WithIncludeExperimental(true),');
});

test('swaps to GleanBuilder and adds includeExperimental in the experimental Java sample', () => {
const experimentalSpec = spec.paths['/api/agents/search'];

expect(
codeSampleTransformer.extractCodeSnippet(experimentalSpec, 'java')
.source,
).toMatchInlineSnapshot(`
"package hello.world;

import com.glean.api_client.glean_api_client.Glean;
import com.glean.api_client.glean_api_client.hooks.GleanBuilder;
import com.glean.api_client.glean_api_client.models.components.PlatformAgentsSearchRequest;
import com.glean.api_client.glean_api_client.models.operations.PlatformAgentsSearchResponse;
import java.lang.Exception;

public class Application {

public static void main(String[] args) throws Exception {

Glean sdk = GleanBuilder.create()
.apiToken(System.getenv().getOrDefault("GLEAN_API_TOKEN", ""))
.includeExperimental(true)
.build();

PlatformAgentsSearchResponse res = sdk.agents().search()
.request(PlatformAgentsSearchRequest.builder()
.name("HR Policy Agent")
.build())
.call();

if (res.platformAgentsSearchResponse().isPresent()) {
System.out.println(res.platformAgentsSearchResponse().get());
}
}
}"
`);
});

test('does not modify non-experimental samples', () => {
const stableSpec = spec.paths['/api/agents/stable'];

const python = codeSampleTransformer.extractCodeSnippet(
stableSpec,
'python',
).source;
const java = codeSampleTransformer.extractCodeSnippet(
stableSpec,
'java',
).source;

expect(python).not.toContain('include_experimental');
expect(java).not.toContain('includeExperimental');
expect(java).not.toContain('GleanBuilder');
expect(java).toContain('Glean.builder()');
});
});
});
Loading
Loading