Skip to content

Commit e252637

Browse files
committed
feat: added xtasks fetch and code-gen to generate code from openapi spec
Also added `compose.yml` to start a typesense server locally for testing
1 parent 5ef6828 commit e252637

File tree

7 files changed

+396
-1
lines changed

7 files changed

+396
-1
lines changed

.cargo/config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[alias]
2+
xtask = "run --package xtask --"

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
/target
22
Cargo.lock
33
.env
4+
5+
/typesense-data

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
members = [
33
"typesense",
44
"typesense_derive",
5-
"typesense_codegen"
5+
"typesense_codegen",
6+
"xtask",
67
]
78

89
resolver = "3"

compose.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
services:
2+
typesense:
3+
image: typesense/typesense:29.0
4+
restart: on-failure
5+
ports:
6+
- '8108:8108'
7+
volumes:
8+
- ./typesense-data:/data
9+
command: '--data-dir /data --api-key=xyz --enable-cors'

xtask/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[package]
2+
name = "xtask"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
reqwest = { version = "0.11", features = ["blocking"] } # "blocking" is simpler for scripts
8+
anyhow = "1.0"
9+
clap = { version = "4.0", features = ["derive"] }
10+
serde = { version = "1.0", features = ["derive"] }
11+
serde_yaml = "0.9"

xtask/src/main.rs

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use anyhow::{Context, Result};
2+
use clap::{Parser, ValueEnum};
3+
use std::env;
4+
use std::fs;
5+
use std::process::Command;
6+
mod preprocess_openapi;
7+
use preprocess_openapi::preprocess_openapi_file;
8+
9+
const SPEC_URL: &str =
10+
"https://raw.githubusercontent.com/typesense/typesense-api-spec/master/openapi.yml";
11+
12+
// Input spec file, expected in the project root.
13+
const INPUT_SPEC_FILE: &str = "openapi.yml";
14+
const OUTPUT_PREPROCESSED_FILE: &str = "./preprocessed_openapi.yml";
15+
16+
// Output directory for the generated code.
17+
const OUTPUT_DIR: &str = "typesense_codegen";
18+
19+
#[derive(Parser)]
20+
#[command(
21+
author,
22+
version,
23+
about = "A task runner for the typesense-rust project"
24+
)]
25+
struct Cli {
26+
/// The list of tasks to run in sequence.
27+
#[arg(required = true, value_enum)]
28+
tasks: Vec<Task>,
29+
}
30+
31+
#[derive(ValueEnum, Clone, Debug)]
32+
#[clap(rename_all = "kebab-case")] // Allows us to type `code-gen` instead of `CodeGen`
33+
enum Task {
34+
/// Fetches the latest OpenAPI spec from [the Typesense repository](https://github.com/typesense/typesense-api-spec/blob/master/openapi.yml).
35+
Fetch,
36+
/// Generates client code from the spec file using the Docker container.
37+
CodeGen,
38+
}
39+
40+
fn main() -> Result<()> {
41+
let cli = Cli::parse();
42+
43+
for task in cli.tasks {
44+
println!("▶️ Running task: {:?}", task);
45+
match task {
46+
Task::Fetch => task_fetch_api_spec()?,
47+
Task::CodeGen => task_codegen()?,
48+
}
49+
}
50+
Ok(())
51+
}
52+
53+
fn task_fetch_api_spec() -> Result<()> {
54+
println!("▶️ Running codegen task...");
55+
56+
println!(" - Downloading spec from {}", SPEC_URL);
57+
let response =
58+
reqwest::blocking::get(SPEC_URL).context("Failed to download OpenAPI spec file")?;
59+
60+
if !response.status().is_success() {
61+
anyhow::bail!("Failed to download spec: HTTP {}", response.status());
62+
}
63+
64+
let spec_content = response.text()?;
65+
fs::write(INPUT_SPEC_FILE, spec_content)
66+
.context(format!("Failed to write spec to {}", INPUT_SPEC_FILE))?;
67+
println!(" - Spec saved to {}", INPUT_SPEC_FILE);
68+
69+
println!("✅ Fetch API spec task finished successfully.");
70+
71+
Ok(())
72+
}
73+
74+
/// Task to generate client code from the OpenAPI spec using a Docker container.
75+
fn task_codegen() -> Result<()> {
76+
println!("▶️ Running codegen task via Docker...");
77+
78+
println!("Preprocessing the Open API spec file...");
79+
preprocess_openapi_file(INPUT_SPEC_FILE, OUTPUT_PREPROCESSED_FILE)
80+
.expect("Preprocess failed, aborting!");
81+
// Get the absolute path to the project's root directory.
82+
// std::env::current_dir() gives us the directory from which `cargo xtask` was run.
83+
let project_root = env::current_dir().context("Failed to get current directory")?;
84+
85+
// Check if the input spec file exists before trying to run Docker.
86+
let input_spec_path = project_root.join(INPUT_SPEC_FILE);
87+
if !input_spec_path.exists() {
88+
anyhow::bail!(
89+
"Input spec '{}' not found in project root. Please add it before running.",
90+
INPUT_SPEC_FILE
91+
);
92+
}
93+
94+
// Construct the volume mount string for Docker.
95+
// Docker needs an absolute path for the volume mount source.
96+
// to_string_lossy() is used to handle potential non-UTF8 paths gracefully.
97+
let volume_mount = format!("{}:/local", project_root.to_string_lossy());
98+
println!(" - Using volume mount: {}", volume_mount);
99+
100+
// Set up and run the Docker command.
101+
println!(" - Starting Docker container...");
102+
let status = Command::new("docker")
103+
.arg("run")
104+
.arg("--rm") // Remove the container after it exits
105+
.arg("-v")
106+
.arg(volume_mount) // Mount the project root to /local in the container
107+
.arg("openapitools/openapi-generator-cli")
108+
.arg("generate")
109+
.arg("-i")
110+
.arg(format!("/local/{}", OUTPUT_PREPROCESSED_FILE)) // Input path inside the container
111+
.arg("-g")
112+
.arg("rust")
113+
.arg("-o")
114+
.arg(format!("/local/{}", OUTPUT_DIR)) // Output path inside the container
115+
.arg("--additional-properties")
116+
.arg("library=reqwest")
117+
.arg("--additional-properties")
118+
.arg("supportMiddleware=true")
119+
.arg("--additional-properties")
120+
.arg("useSingleRequestParameter=true")
121+
// .arg("--additional-properties")
122+
// .arg("useBonBuilder=true")
123+
.status()
124+
.context("Failed to execute Docker command. Is Docker installed and running?")?;
125+
126+
// Check if the command was successful.
127+
if !status.success() {
128+
anyhow::bail!("Docker command failed with status: {}", status);
129+
}
130+
131+
println!("✅ Codegen task finished successfully.");
132+
println!(" Generated code is available in '{}'", OUTPUT_DIR);
133+
Ok(())
134+
}

0 commit comments

Comments
 (0)