Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.

Commit 36fd426

Browse files
merge: #3308
3308: feat(si-generator): Adds si-generator r=stack72 a=adamhjk This adds a prototype of `si-generator`, a deno based cli that generates assets. Initially it only works for AWS. You can use it by calling: ``` deno run --allow-run ./main.ts s3api create-bucket ``` You can also compile a standlone binary with: ``` deno task compile ``` And run the (very minimal) test suite with: ``` deno task test ``` Co-authored-by: Adam Jacob <adam@systeminit.com>
2 parents 6472365 + 8a42b2f commit 36fd426

File tree

19 files changed

+633
-0
lines changed

19 files changed

+633
-0
lines changed

bin/si-generator/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
out

bin/si-generator/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# This is a spike
2+
3+
It doesn't have buck2 support, we don't actually use it much yet.
4+
5+
But it works!
6+
7+
# To use
8+
9+
```
10+
$ deno run --allow-run main.ts asset s3api create-bucket
11+
```
12+
13+
Would print an asset definition function for the s3api create-bucket call.

bin/si-generator/deno.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"tasks": {
3+
"test": "deno test --allow-run",
4+
"dev": "deno run --watch main.ts",
5+
"compile": "deno compile --allow-run --output ./out/si-generate main.ts"
6+
}
7+
}

bin/si-generator/deno.lock

Lines changed: 231 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/si-generator/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { run } from "./src/run.ts";
2+
3+
if (import.meta.main) {
4+
await run();
5+
}

bin/si-generator/main_test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { assertEquals } from "https://deno.land/std@0.215.0/assert/mod.ts";
2+
import { awsGenerate } from "./src/asset_generator.ts";
3+
import { Prop } from "./src/props.ts";
4+
5+
Deno.test(function awsServiceProps() {
6+
const correctProps: Array<Prop> = [
7+
{ kind: "string", name: "KeyName", variableName: "keyNameProp" },
8+
{ kind: "boolean", name: "DryRun", variableName: "dryRunProp" },
9+
{ kind: "string", name: "KeyType", variableName: "keyTypeProp" },
10+
{
11+
kind: "array",
12+
name: "TagSpecifications",
13+
variableName: "tagSpecificationsProp",
14+
entry: {
15+
kind: "object",
16+
name: "TagSpecificationsChild",
17+
variableName: "tagSpecificationsChildProp",
18+
children: [
19+
{
20+
kind: "string",
21+
name: "ResourceType",
22+
variableName: "resourceTypeProp",
23+
},
24+
{
25+
kind: "array",
26+
name: "Tags",
27+
variableName: "tagsProp",
28+
entry: {
29+
kind: "object",
30+
name: "TagsChild",
31+
variableName: "tagsChildProp",
32+
children: [
33+
{ kind: "string", name: "Key", variableName: "keyProp" },
34+
{
35+
kind: "string",
36+
name: "Value",
37+
variableName: "valueProp",
38+
},
39+
],
40+
},
41+
},
42+
],
43+
},
44+
},
45+
{ kind: "string", name: "KeyFormat", variableName: "keyFormatProp" },
46+
];
47+
const props = awsGenerate("ec2", "create-key-pair");
48+
assertEquals(props, correctProps);
49+
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { Prop, PropParent } from "./props.ts";
2+
import { camelCase } from "https://deno.land/x/case/mod.ts";
3+
import { singular } from "https://deno.land/x/deno_plural/mod.ts";
4+
5+
type AwsScaffold = Record<string, unknown>;
6+
7+
export function awsGenerate(
8+
awsService: string,
9+
awsCommand: string,
10+
): Array<Prop> {
11+
const scaffold = getAwsCliScaffold(awsService, awsCommand);
12+
const props = propsFromScaffold(scaffold, []);
13+
return props;
14+
}
15+
16+
function getAwsCliScaffold(
17+
awsService: string,
18+
awsCommand: string,
19+
): AwsScaffold {
20+
const command = new Deno.Command("aws", {
21+
args: [awsService, awsCommand, "--generate-cli-skeleton"],
22+
stdin: "null",
23+
stdout: "piped",
24+
stderr: "piped",
25+
});
26+
const { code, stdout: rawStdout, stderr: rawStderr } = command.outputSync();
27+
const stdout = new TextDecoder().decode(rawStdout);
28+
const stderr = new TextDecoder().decode(rawStderr);
29+
30+
if (code !== 0) {
31+
console.error(`AWS cli failed with exit code: ${code}`);
32+
console.error(`STDOUT:\n\n${stdout.toLocaleString()}`);
33+
console.error(`STDERR:\n\n${stderr.toLocaleString()}`);
34+
throw new Error("aws cli command failed");
35+
}
36+
const result = JSON.parse(stdout);
37+
return result;
38+
}
39+
40+
function propsFromScaffold(
41+
scaffold: AwsScaffold,
42+
props: Array<Prop>,
43+
parent?: PropParent,
44+
): Array<Prop> {
45+
for (let [key, value] of Object.entries(scaffold)) {
46+
if (
47+
key == "KeyName" && parent?.kind == "object" &&
48+
parent?.children.length == 0
49+
) {
50+
// @ts-ignore we know you can't do this officialy, but unofficialy, suck
51+
// it.
52+
parent.kind = "map";
53+
key = singular(parent.name);
54+
}
55+
let prop: Prop | undefined;
56+
if (typeof value === "string") {
57+
prop = {
58+
kind: "string",
59+
name: key,
60+
variableName: camelCase(`${key}Prop`),
61+
};
62+
} else if (typeof value === "number") {
63+
prop = {
64+
kind: "number",
65+
name: key,
66+
variableName: camelCase(`${key}Prop`),
67+
};
68+
} else if (typeof value === "boolean") {
69+
prop = {
70+
kind: "boolean",
71+
name: key,
72+
variableName: camelCase(`${key}Prop`),
73+
};
74+
} else if (Array.isArray(value)) {
75+
prop = {
76+
kind: "array",
77+
name: key,
78+
variableName: camelCase(`${key}Prop`),
79+
};
80+
const childObject: AwsScaffold = {};
81+
childObject[`${key}Child`] = value[0];
82+
propsFromScaffold(childObject, props, prop);
83+
} else if (value == null) {
84+
// Sometimes the default value is null, and not the empty string. This
85+
// seems like a reasonable default, even if it is going to be weird.
86+
prop = {
87+
kind: "string",
88+
name: key,
89+
variableName: camelCase(`${key}Prop`),
90+
};
91+
} else if (typeof value === "object") {
92+
prop = {
93+
kind: "object",
94+
name: key,
95+
variableName: camelCase(`${key}Prop`),
96+
children: [],
97+
};
98+
propsFromScaffold(value as AwsScaffold, props, prop);
99+
}
100+
if (prop && parent?.kind == "object") {
101+
parent.children.push(prop);
102+
} else if (prop && parent?.kind == "array" || parent?.kind == "map") {
103+
parent.entry = prop;
104+
} else if (prop && !parent) {
105+
props.push(prop);
106+
}
107+
}
108+
return props;
109+
}

bin/si-generator/src/props.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
export interface PropBase {
2+
kind: string;
3+
name: string;
4+
variableName: string;
5+
}
6+
7+
export interface PropString extends PropBase {
8+
kind: "string";
9+
}
10+
11+
export interface PropNumber extends PropBase {
12+
kind: "number";
13+
}
14+
15+
export interface PropBoolean extends PropBase {
16+
kind: "boolean";
17+
}
18+
19+
export interface PropObject extends PropBase {
20+
kind: "object";
21+
children: Array<Prop>;
22+
}
23+
24+
export interface PropMap extends PropBase {
25+
kind: "map";
26+
entry?: Prop;
27+
}
28+
29+
export interface PropArray extends PropBase {
30+
kind: "array";
31+
entry?: Prop;
32+
}
33+
34+
export type Prop =
35+
| PropString
36+
| PropNumber
37+
| PropObject
38+
| PropArray
39+
| PropBoolean
40+
| PropMap;
41+
42+
export type PropParent = PropObject | PropArray | PropMap;

bin/si-generator/src/render.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { Eta } from "https://deno.land/x/eta@v3.2.0/src/index.ts";
2+
import { Prop } from "./props.ts";
3+
import { partial as assetMainPartial } from "./templates/assetMain.ts";
4+
import { partial as arrayPartial } from "./templates/array.ts";
5+
import { partial as booleanPartial } from "./templates/boolean.ts";
6+
import { partial as mapPartial } from "./templates/map.ts";
7+
import { partial as numberPartial } from "./templates/number.ts";
8+
import { partial as objectPartial } from "./templates/object.ts";
9+
import { partial as stringPartial } from "./templates/string.ts";
10+
import { partial as renderPropPartial } from "./templates/renderProp.ts";
11+
12+
type RenderProvider = "aws";
13+
14+
export async function renderAsset(props: Array<Prop>, provider: RenderProvider): Promise<string> {
15+
const eta = new Eta({
16+
debug: true,
17+
autoEscape: false,
18+
});
19+
eta.loadTemplate("@assetMain", assetMainPartial);
20+
eta.loadTemplate("@arrayPartial", arrayPartial);
21+
eta.loadTemplate("@booleanPartial", booleanPartial);
22+
eta.loadTemplate("@mapPartial", mapPartial);
23+
eta.loadTemplate("@numberPartial", numberPartial);
24+
eta.loadTemplate("@objectPartial", objectPartial);
25+
eta.loadTemplate("@stringPartial", stringPartial);
26+
eta.loadTemplate("@renderPropPartial", renderPropPartial);
27+
const assetDefinition = eta.render("@assetMain", { props, provider });
28+
29+
const command = new Deno.Command("deno", {
30+
args: ["fmt", "-"],
31+
stdin: "piped",
32+
stdout: "piped",
33+
stderr: "piped",
34+
});
35+
const running = command.spawn();
36+
const writer = running.stdin.getWriter();
37+
await writer.write(new TextEncoder().encode(assetDefinition));
38+
writer.releaseLock();
39+
await running.stdin.close();
40+
41+
const n = await running.stdout.getReader().read();
42+
const stdout = new TextDecoder().decode(n.value);
43+
const result = await running.status;
44+
if (result.success) {
45+
return stdout;
46+
} else {
47+
return assetDefinition;
48+
}
49+
}

bin/si-generator/src/run.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Command } from "https://deno.land/x/cliffy@v1.0.0-rc.3/command/mod.ts";
2+
import { awsGenerate } from "./asset_generator.ts";
3+
import { renderAsset } from "./render.ts";
4+
5+
export async function run() {
6+
const command = new Command()
7+
.name("si-generator")
8+
.version("0.1.0")
9+
.description(
10+
"Generate Assets and code for System Initiative",
11+
)
12+
.action(() => {
13+
command.showHelp();
14+
Deno.exit(1);
15+
})
16+
.command("asset")
17+
.description("generate an asset definition from an aws cli skeleton")
18+
.arguments("<awsService:string> <awsCommand:string>")
19+
.action(async (_options, awsService, awsCommand) => {
20+
const props = awsGenerate(awsService, awsCommand);
21+
const result = await renderAsset(props, "aws");
22+
console.log(result);
23+
});
24+
const _result = await command.parse(Deno.args);
25+
}

0 commit comments

Comments
 (0)