Skip to content

Commit 1a73208

Browse files
authored
feat: added xtasks fetch and code-gen to generate code (#51)
* 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 * fix `Cargo.toml` package metadata * lint fix * fix: avoid compiling xtask for wasm * fix fmt and typesense/lib.rs docs test
1 parent 5ef6828 commit 1a73208

File tree

9 files changed

+445
-49
lines changed

9 files changed

+445
-49
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'

typesense/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//! # Examples
99
//!
1010
//! ```
11-
//! #[cfg(any(feature = "tokio_test", target_arch = "wasm32"))]
11+
//! #[cfg(not(target_family = "wasm"))]
1212
//! {
1313
//! use serde::{Deserialize, Serialize};
1414
//! use typesense::document::Document;

typesense_derive/src/lib.rs

Lines changed: 42 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,17 @@ fn impl_typesense_collection(item: ItemStruct) -> syn::Result<TokenStream> {
4343
} = extract_attrs(attrs)?;
4444
let collection_name = collection_name.unwrap_or_else(|| ident.to_string().to_lowercase());
4545

46-
if let Some(ref sorting_field) = default_sorting_field {
47-
if !fields.iter().any(|field|
46+
if let Some(ref sorting_field) = default_sorting_field
47+
&& !fields.iter().any(|field|
4848
// At this point we are sure that this field is a named field.
4949
field.ident.as_ref().unwrap() == sorting_field)
50-
{
51-
return Err(syn::Error::new_spanned(
52-
item_ts,
53-
format!(
54-
"defined default_sorting_field = \"{sorting_field}\" does not match with any field."
55-
),
56-
));
57-
}
50+
{
51+
return Err(syn::Error::new_spanned(
52+
item_ts,
53+
format!(
54+
"defined default_sorting_field = \"{sorting_field}\" does not match with any field."
55+
),
56+
));
5857
}
5958

6059
let typesense_fields = fields
@@ -98,17 +97,16 @@ fn impl_typesense_collection(item: ItemStruct) -> syn::Result<TokenStream> {
9897

9998
// Get the inner type for a given wrapper
10099
fn ty_inner_type<'a>(ty: &'a syn::Type, wrapper: &'static str) -> Option<&'a syn::Type> {
101-
if let syn::Type::Path(p) = ty {
102-
if p.path.segments.len() == 1 && p.path.segments[0].ident == wrapper {
103-
if let syn::PathArguments::AngleBracketed(ref inner_ty) = p.path.segments[0].arguments {
104-
if inner_ty.args.len() == 1 {
105-
// len is 1 so this should not fail
106-
let inner_ty = inner_ty.args.first().unwrap();
107-
if let syn::GenericArgument::Type(t) = inner_ty {
108-
return Some(t);
109-
}
110-
}
111-
}
100+
if let syn::Type::Path(p) = ty
101+
&& p.path.segments.len() == 1
102+
&& p.path.segments[0].ident == wrapper
103+
&& let syn::PathArguments::AngleBracketed(ref inner_ty) = p.path.segments[0].arguments
104+
&& inner_ty.args.len() == 1
105+
{
106+
// len is 1 so this should not fail
107+
let inner_ty = inner_ty.args.first().unwrap();
108+
if let syn::GenericArgument::Type(t) = inner_ty {
109+
return Some(t);
112110
}
113111
}
114112
None
@@ -231,42 +229,39 @@ fn to_typesense_field_type(field: &Field) -> syn::Result<proc_macro2::TokenStrea
231229
.attrs
232230
.iter()
233231
.filter_map(|attr| {
234-
if attr.path.segments.len() == 1 && attr.path.segments[0].ident == "typesense" {
235-
if let Some(proc_macro2::TokenTree::Group(g)) =
232+
if attr.path.segments.len() == 1
233+
&& attr.path.segments[0].ident == "typesense"
234+
&& let Some(proc_macro2::TokenTree::Group(g)) =
236235
attr.tokens.clone().into_iter().next()
237-
{
238-
let mut tokens = g.stream().into_iter();
239-
match tokens.next() {
240-
Some(proc_macro2::TokenTree::Ident(ref i)) => {
241-
if i != "facet" {
242-
return Some(Err(syn::Error::new_spanned(
243-
i,
244-
format!("Unexpected token {i}. Did you mean `facet`?"),
245-
)));
246-
}
247-
}
248-
Some(ref tt) => {
249-
return Some(Err(syn::Error::new_spanned(
250-
tt,
251-
format!("Unexpected token {tt}. Did you mean `facet`?"),
252-
)));
253-
}
254-
None => {
236+
{
237+
let mut tokens = g.stream().into_iter();
238+
match tokens.next() {
239+
Some(proc_macro2::TokenTree::Ident(ref i)) => {
240+
if i != "facet" {
255241
return Some(Err(syn::Error::new_spanned(
256-
attr,
257-
"expected `facet`",
242+
i,
243+
format!("Unexpected token {i}. Did you mean `facet`?"),
258244
)));
259245
}
260246
}
261-
262-
if let Some(ref tt) = tokens.next() {
247+
Some(ref tt) => {
263248
return Some(Err(syn::Error::new_spanned(
264249
tt,
265-
"Unexpected token. Expected )",
250+
format!("Unexpected token {tt}. Did you mean `facet`?"),
266251
)));
267252
}
268-
return Some(Ok(()));
253+
None => {
254+
return Some(Err(syn::Error::new_spanned(attr, "expected `facet`")));
255+
}
256+
}
257+
258+
if let Some(ref tt) = tokens.next() {
259+
return Some(Err(syn::Error::new_spanned(
260+
tt,
261+
"Unexpected token. Expected )",
262+
)));
269263
}
264+
return Some(Ok(()));
270265
}
271266
None
272267
})

xtask/Cargo.toml

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

xtask/src/main.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
#[cfg(target_family = "wasm")]
41+
fn main() {}
42+
43+
#[cfg(not(target_family = "wasm"))]
44+
fn main() -> Result<()> {
45+
let cli = Cli::parse();
46+
47+
for task in cli.tasks {
48+
println!("▶️ Running task: {:?}", task);
49+
match task {
50+
Task::Fetch => task_fetch_api_spec()?,
51+
Task::CodeGen => task_codegen()?,
52+
}
53+
}
54+
Ok(())
55+
}
56+
57+
#[cfg(not(target_family = "wasm"))]
58+
fn task_fetch_api_spec() -> Result<()> {
59+
println!("▶️ Running codegen task...");
60+
61+
println!(" - Downloading spec from {}", SPEC_URL);
62+
let response =
63+
reqwest::blocking::get(SPEC_URL).context("Failed to download OpenAPI spec file")?;
64+
65+
if !response.status().is_success() {
66+
anyhow::bail!("Failed to download spec: HTTP {}", response.status());
67+
}
68+
69+
let spec_content = response.text()?;
70+
fs::write(INPUT_SPEC_FILE, spec_content)
71+
.context(format!("Failed to write spec to {}", INPUT_SPEC_FILE))?;
72+
println!(" - Spec saved to {}", INPUT_SPEC_FILE);
73+
74+
println!("✅ Fetch API spec task finished successfully.");
75+
76+
Ok(())
77+
}
78+
79+
/// Task to generate client code from the OpenAPI spec using a Docker container.
80+
fn task_codegen() -> Result<()> {
81+
println!("▶️ Running codegen task via Docker...");
82+
83+
println!("Preprocessing the Open API spec file...");
84+
preprocess_openapi_file(INPUT_SPEC_FILE, OUTPUT_PREPROCESSED_FILE)
85+
.expect("Preprocess failed, aborting!");
86+
// Get the absolute path to the project's root directory.
87+
// std::env::current_dir() gives us the directory from which `cargo xtask` was run.
88+
let project_root = env::current_dir().context("Failed to get current directory")?;
89+
90+
// Check if the input spec file exists before trying to run Docker.
91+
let input_spec_path = project_root.join(INPUT_SPEC_FILE);
92+
if !input_spec_path.exists() {
93+
anyhow::bail!(
94+
"Input spec '{}' not found in project root. Please add it before running.",
95+
INPUT_SPEC_FILE
96+
);
97+
}
98+
99+
// Construct the volume mount string for Docker.
100+
// Docker needs an absolute path for the volume mount source.
101+
// to_string_lossy() is used to handle potential non-UTF8 paths gracefully.
102+
let volume_mount = format!("{}:/local", project_root.to_string_lossy());
103+
println!(" - Using volume mount: {}", volume_mount);
104+
105+
// Set up and run the Docker command.
106+
println!(" - Starting Docker container...");
107+
let status = Command::new("docker")
108+
.arg("run")
109+
.arg("--rm") // Remove the container after it exits
110+
.arg("-v")
111+
.arg(volume_mount) // Mount the project root to /local in the container
112+
.arg("openapitools/openapi-generator-cli")
113+
.arg("generate")
114+
.arg("-i")
115+
.arg(format!("/local/{}", OUTPUT_PREPROCESSED_FILE)) // Input path inside the container
116+
.arg("-g")
117+
.arg("rust")
118+
.arg("-o")
119+
.arg(format!("/local/{}", OUTPUT_DIR)) // Output path inside the container
120+
.arg("--additional-properties")
121+
.arg("library=reqwest")
122+
.arg("--additional-properties")
123+
.arg("supportMiddleware=true")
124+
.arg("--additional-properties")
125+
.arg("useSingleRequestParameter=true")
126+
// .arg("--additional-properties")
127+
// .arg("useBonBuilder=true")
128+
.status()
129+
.context("Failed to execute Docker command. Is Docker installed and running?")?;
130+
131+
// Check if the command was successful.
132+
if !status.success() {
133+
anyhow::bail!("Docker command failed with status: {}", status);
134+
}
135+
136+
println!("✅ Codegen task finished successfully.");
137+
println!(" Generated code is available in '{}'", OUTPUT_DIR);
138+
Ok(())
139+
}

0 commit comments

Comments
 (0)