Skip to content

Commit 9401fb0

Browse files
committed
Move the current CLI out of main.rs and into cli_v1.rs
I have plans for a CLI v2 🤫🤫
1 parent 43fcf9f commit 9401fb0

File tree

7 files changed

+228
-224
lines changed

7 files changed

+228
-224
lines changed

src/api/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pub(crate) mod types;
77
use aide::openapi;
88
use serde::{Deserialize, Serialize};
99

10-
use crate::IncludeMode;
10+
use crate::cli_v1::IncludeMode;
1111

1212
pub(crate) use self::{
1313
resources::{Resource, Resources},

src/api/resources.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ use indexmap::IndexMap;
66
use schemars::schema::{InstanceType, Schema};
77
use serde::{Deserialize, Serialize};
88

9+
use crate::cli_v1::IncludeMode;
10+
911
use super::{
1012
get_schema_name,
1113
types::{FieldType, serialize_field_type},
1214
};
13-
use crate::IncludeMode;
1415

1516
/// The API operations of the API client we generate.
1617
///

src/api/types.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ use schemars::schema::{
1212
};
1313
use serde::{Deserialize, Serialize};
1414

15+
use crate::cli_v1::IncludeMode;
16+
1517
use super::{
1618
get_schema_name,
1719
resources::{self, Resources},
1820
};
19-
use crate::IncludeMode;
2021

2122
/// Named types referenced by API operations.
2223
///

src/cli_v1.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
use std::{
2+
collections::BTreeSet,
3+
io,
4+
path::{Path, PathBuf},
5+
};
6+
7+
use aide::openapi::OpenApi;
8+
use anyhow::{Context as _, bail};
9+
use camino::Utf8PathBuf;
10+
use clap::{Parser, Subcommand};
11+
use fs_err::{self as fs};
12+
use schemars::schema::Schema;
13+
use tempfile::TempDir;
14+
15+
use crate::{api::Api, generator::generate};
16+
17+
#[derive(Parser)]
18+
struct CliArgs {
19+
/// Which operations to include.
20+
#[arg(global = true, long, value_enum, default_value_t = IncludeMode::OnlyPublic)]
21+
include_mode: IncludeMode,
22+
23+
/// Ignore a specified operation id
24+
#[arg(global = true, short, long = "exclude-op-id")]
25+
excluded_operations: Vec<String>,
26+
27+
/// Only include specified operations
28+
///
29+
/// This option only works with `--include-mode=only-specified`.
30+
///
31+
/// Use this option, to run the codegen with a limited set of operations.
32+
/// Op webhook models will be excluded from the generation
33+
#[arg(global = true, long = "include-op-id")]
34+
specified_operations: Vec<String>,
35+
36+
#[command(subcommand)]
37+
command: Command,
38+
}
39+
40+
#[derive(Clone, Subcommand)]
41+
enum Command {
42+
/// Generate code from an OpenAPI spec.
43+
Generate {
44+
/// Path to a template file to use (`.jinja` extension can be omitted).
45+
#[arg(short, long)]
46+
template: Utf8PathBuf,
47+
48+
/// Path to the input file(s).
49+
#[arg(short, long)]
50+
input_file: Vec<String>,
51+
52+
/// Path to the output directory.
53+
#[arg(short, long)]
54+
output_dir: Option<Utf8PathBuf>,
55+
56+
/// Disable automatic postprocessing of the output (formatting and automatic style fixes).
57+
#[arg(long)]
58+
no_postprocess: bool,
59+
},
60+
/// Generate api.ron and types.ron files, for debugging.
61+
Debug {
62+
/// Path to the input file(s).
63+
#[arg(short, long)]
64+
input_file: Vec<String>,
65+
},
66+
}
67+
68+
#[derive(Copy, Clone, clap::ValueEnum)]
69+
#[clap(rename_all = "kebab-case")]
70+
pub(crate) enum IncludeMode {
71+
/// Only public options
72+
OnlyPublic,
73+
/// Both public operations and operations marked with `x-internal`
74+
PublicAndInternal,
75+
/// Only operations marked with `x-internal`
76+
OnlyInternal,
77+
/// Only operations that were specified in `--include-op-id`
78+
OnlySpecified,
79+
}
80+
81+
pub fn run_cli_v1_main() -> anyhow::Result<()> {
82+
tracing_subscriber::fmt().with_writer(io::stderr).init();
83+
84+
let args = CliArgs::parse();
85+
86+
let excluded_operations = BTreeSet::from_iter(args.excluded_operations);
87+
let specified_operations = BTreeSet::from_iter(args.specified_operations);
88+
89+
let input_files = match &args.command {
90+
Command::Generate { input_file, .. } => input_file,
91+
Command::Debug { input_file } => input_file,
92+
};
93+
94+
let api = input_files
95+
.iter()
96+
.map(|input_file| {
97+
let input_file = Path::new(input_file);
98+
let input_file_ext = input_file
99+
.extension()
100+
.context("input file must have a file extension")?;
101+
let input_file_contents = fs::read_to_string(input_file)?;
102+
103+
if input_file_ext == "json" {
104+
let spec: OpenApi = serde_json::from_str(&input_file_contents)
105+
.context("failed to parse OpenAPI spec")?;
106+
107+
let webhooks = get_webhooks(&spec);
108+
Api::new(
109+
spec.paths.context("found no endpoints in input spec")?,
110+
&mut spec.components.unwrap_or_default(),
111+
&webhooks,
112+
args.include_mode,
113+
&excluded_operations,
114+
&specified_operations,
115+
)
116+
.context("converting OpenAPI spec to our own representation")
117+
} else if input_file_ext == "ron" {
118+
ron::from_str(&input_file_contents).context("parsing ron file")
119+
} else {
120+
bail!("input file extension must be .json or .ron");
121+
}
122+
})
123+
.collect::<anyhow::Result<Api>>()?;
124+
125+
match args.command {
126+
Command::Generate {
127+
template,
128+
output_dir,
129+
no_postprocess,
130+
..
131+
} => {
132+
let generated_paths = match &output_dir {
133+
Some(path) => {
134+
let generated_paths = generate(api, template.into(), path, no_postprocess)?;
135+
println!("done! output written to {path}");
136+
generated_paths
137+
}
138+
None => {
139+
let output_dir_root = PathBuf::from("out");
140+
if !output_dir_root.exists() {
141+
fs::create_dir(&output_dir_root).context("failed to create out dir")?;
142+
}
143+
144+
let tpl_file_name = template
145+
.file_name()
146+
.context("template must have a file name")?;
147+
let prefix = tpl_file_name
148+
.strip_suffix(".jinja")
149+
.unwrap_or(tpl_file_name);
150+
151+
let output_dir =
152+
TempDir::with_prefix_in(prefix.to_owned() + ".", output_dir_root)
153+
.context("failed to create tempdir")?;
154+
155+
let path = output_dir
156+
.path()
157+
.try_into()
158+
.context("non-UTF8 tempdir path")?;
159+
160+
let generated_paths = generate(api, template.into(), path, no_postprocess)?;
161+
println!("done! output written to {path}");
162+
163+
// Persist the TempDir if everything was successful
164+
_ = output_dir.keep();
165+
generated_paths
166+
}
167+
};
168+
let paths: Vec<&str> = generated_paths.iter().map(|p| p.as_str()).collect();
169+
let serialized = serde_json::to_string_pretty(&paths)?;
170+
fs::write(".generated_paths.json", serialized)?;
171+
}
172+
Command::Debug { .. } => {
173+
let serialized = ron::ser::to_string_pretty(&api, Default::default())?;
174+
fs::write("debug.ron", serialized)?;
175+
}
176+
}
177+
178+
Ok(())
179+
}
180+
181+
fn get_webhooks(spec: &OpenApi) -> Vec<String> {
182+
let empty_obj = serde_json::Map::new();
183+
let mut referenced_components = std::collections::BTreeSet::<String>::new();
184+
if let Some(webhooks) = spec.extensions.get("x-webhooks") {
185+
for req in webhooks.as_object().unwrap_or(&empty_obj).values() {
186+
for method in req.as_object().unwrap_or(&empty_obj).values() {
187+
if let Some(schema_ref) =
188+
method["requestBody"]["content"]["application/json"]["schema"]["$ref"].as_str()
189+
&& let Some(schema_name) = schema_ref.split('/').next_back()
190+
{
191+
referenced_components.insert(schema_name.to_string());
192+
}
193+
}
194+
}
195+
}
196+
197+
// also check the spec.webhooks
198+
for (_, webhook) in &spec.webhooks {
199+
let Some(item) = webhook.as_item() else {
200+
continue;
201+
};
202+
203+
for (_, op) in item.iter() {
204+
if let Some(body) = &op.request_body
205+
&& let Some(item) = body.as_item()
206+
&& let Some(json_content) = item.content.get("application/json")
207+
&& let Some(schema) = &json_content.schema
208+
&& let Schema::Object(obj) = &schema.json_schema
209+
&& let Some(reference) = &obj.reference
210+
&& let Some(component_name) = reference.split('/').next_back()
211+
{
212+
referenced_components.insert(component_name.to_owned());
213+
}
214+
}
215+
}
216+
217+
referenced_components.into_iter().collect::<Vec<String>>()
218+
}

src/codesamples.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::{
1010
Api, Resource,
1111
types::{EnumVariantType, Field, FieldType, StructEnumRepr, Type, TypeData},
1212
},
13+
cli_v1::IncludeMode,
1314
template,
1415
};
1516
use aide::openapi::OpenApi;
@@ -202,7 +203,7 @@ pub async fn generate_codesamples(
202203
.context("found no endpoints in input spec")?,
203204
&mut openapi_spec.components.clone().unwrap_or_default(),
204205
&[],
205-
crate::IncludeMode::OnlyPublic,
206+
IncludeMode::OnlyPublic,
206207
&excluded_operation_ids,
207208
&BTreeSet::new(),
208209
)?;

0 commit comments

Comments
 (0)