Skip to content

Commit e4e309a

Browse files
committed
feat: Add Ruby code generation target
1 parent 169286f commit e4e309a

21 files changed

Lines changed: 1196 additions & 6 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Generated SDKs (pushed to separate repos)
55
/xdk/python/
66
/xdk/typescript/
7+
/xdk/ruby/
78

89
# Deploy keys (never commit)
910
/.keys/

Makefile

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# XDK SDK Generator
22

33
.PHONY: all check build test clean help
4-
.PHONY: generate python typescript
5-
.PHONY: test-python test-typescript test-sdks
4+
.PHONY: generate python typescript ruby
5+
.PHONY: test-python test-typescript test-ruby test-sdks
66
.PHONY: fmt clippy test-generator
77
.PHONY: versions
88

@@ -16,14 +16,17 @@ all: check test-generator
1616
# SDK Generation (local dev)
1717
# =====================================
1818

19-
generate: python typescript
19+
generate: python typescript ruby
2020

2121
python:
2222
cargo run -- python --latest true
2323

2424
typescript:
2525
cargo run -- typescript --latest true
2626

27+
ruby:
28+
cargo run -- ruby --latest true
29+
2730
# =====================================
2831
# SDK Testing (local dev)
2932
# =====================================
@@ -36,6 +39,9 @@ test-python: python
3639
test-typescript: typescript
3740
cd xdk/typescript && npm ci && npm run build && npm run type-check && npm test
3841

42+
test-ruby: ruby
43+
cd xdk/ruby && bundle install && bundle exec rspec spec/ --format documentation
44+
3945
# =====================================
4046
# Generator
4147
# =====================================
@@ -61,14 +67,14 @@ test: test-generator test-sdks
6167
# =====================================
6268

6369
versions:
64-
@grep -E "^(python|typescript) = " xdk-config.toml
70+
@grep -E "^(python|typescript|ruby) = " xdk-config.toml
6571

6672
# =====================================
6773
# Cleanup
6874
# =====================================
6975

7076
clean:
71-
rm -rf xdk/python xdk/typescript
77+
rm -rf xdk/python xdk/typescript xdk/ruby
7278

7379
cargo-clean:
7480
cargo clean
@@ -82,8 +88,10 @@ help:
8288
@echo ""
8389
@echo "Local Development:"
8490
@echo " make python Generate Python SDK"
91+
@echo " make ruby Generate Ruby SDK"
8592
@echo " make typescript Generate TypeScript SDK"
8693
@echo " make test-python Generate + test Python SDK"
94+
@echo " make test-ruby Generate + test Ruby SDK"
8795
@echo " make test-typescript Generate + test TypeScript SDK"
8896
@echo ""
8997
@echo "Generator:"

xdk-build/src/main.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Declare modules
55
mod error;
66
mod python;
7+
mod ruby;
78
mod typescript;
89
mod utils;
910

@@ -50,6 +51,19 @@ enum Commands {
5051
#[arg(short, long, default_value = "xdk/typescript")]
5152
output: PathBuf,
5253
},
54+
/// Generate a Ruby SDK from an OpenAPI specification
55+
Ruby {
56+
/// Path to the OpenAPI specification file
57+
#[arg(short, long)]
58+
spec: Option<PathBuf>,
59+
60+
#[arg(short, long)]
61+
latest: Option<bool>,
62+
63+
/// Output directory for the generated SDK
64+
#[arg(short, long, default_value = "xdk/ruby")]
65+
output: PathBuf,
66+
},
5367
}
5468

5569
#[tokio::main]
@@ -164,6 +178,51 @@ async fn main() -> Result<()> {
164178
// Call the generate method - `?` handles the Result conversion
165179
typescript::generate(&openapi, &output)
166180
}
181+
Commands::Ruby {
182+
spec,
183+
output,
184+
latest,
185+
} => {
186+
let openapi = if latest == Some(true) {
187+
let client = reqwest::Client::new();
188+
let response = client
189+
.get("https://api.x.com/2/openapi.json")
190+
.send()
191+
.await
192+
.map_err(|e| {
193+
BuildError::CommandFailed(format!("Failed to fetch OpenAPI spec: {}", e))
194+
})?;
195+
196+
let json_text = response.text().await.map_err(|e| {
197+
BuildError::CommandFailed(format!("Failed to read response: {}", e))
198+
})?;
199+
200+
parse_json(&json_text).map_err(|e| SdkGeneratorError::from(e.to_string()))?
201+
} else {
202+
let extension = spec
203+
.as_ref()
204+
.unwrap()
205+
.extension()
206+
.and_then(|ext| ext.to_str())
207+
.ok_or_else(|| {
208+
BuildError::CommandFailed("Invalid file extension".to_string())
209+
})?;
210+
211+
match extension {
212+
"yaml" | "yml" => parse_yaml_file(spec.as_ref().unwrap().to_str().unwrap())
213+
.map_err(|e| SdkGeneratorError::from(e.to_string()))?,
214+
"json" => parse_json_file(spec.as_ref().unwrap().to_str().unwrap())
215+
.map_err(|e| SdkGeneratorError::from(e.to_string()))?,
216+
_ => {
217+
let err_msg = format!("Unsupported file extension: {}", extension);
218+
return Err(BuildError::CommandFailed(err_msg));
219+
}
220+
}
221+
};
222+
223+
log_info!("Specification parsed successfully.");
224+
ruby::generate(&openapi, &output)
225+
}
167226
};
168227

169228
// Handle the result with better error messaging

xdk-build/src/ruby.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use crate::error::{BuildError, Result};
2+
use colored::*;
3+
use std::path::Path;
4+
use std::process::Command;
5+
use xdk_gen::Ruby;
6+
use xdk_lib::{XdkConfig, log_error, log_info, log_success};
7+
use xdk_openapi::OpenApi;
8+
9+
/// Generates the Ruby SDK.
10+
pub fn generate(openapi: &OpenApi, output_dir: &Path) -> Result<()> {
11+
log_info!("Generating Ruby SDK code...");
12+
13+
// Load configuration to get version
14+
let config = XdkConfig::load_default().map_err(BuildError::SdkGenError)?;
15+
let version = config.get_version("ruby").ok_or_else(|| {
16+
BuildError::SdkGenError(xdk_lib::SdkGeneratorError::FrameworkError(
17+
"Ruby version not found in config".to_string(),
18+
))
19+
})?;
20+
21+
// Create output directory if it doesn't exist
22+
if let Err(e) = std::fs::create_dir_all(output_dir) {
23+
log_error!(
24+
"Failed to create output directory '{}': {}",
25+
output_dir.display(),
26+
e
27+
);
28+
return Err(BuildError::IoError(e));
29+
}
30+
31+
// Generate the SDK code
32+
if let Err(e) = xdk_lib::generator::generate(Ruby, openapi, output_dir, version) {
33+
log_error!("Failed to generate Ruby SDK code: {}", e);
34+
return Err(BuildError::SdkGenError(e));
35+
}
36+
log_success!("SDK code generated.");
37+
38+
// Optionally run rubocop for formatting (if available)
39+
if which_exists("rubocop") {
40+
log_info!("Formatting code with rubocop...");
41+
let mut fmt_cmd = Command::new("rubocop");
42+
fmt_cmd.arg("-a").arg(output_dir);
43+
// Best-effort formatting; don't fail the build if rubocop isn't configured
44+
let _ = fmt_cmd.output();
45+
log_success!("Rubocop formatting complete.");
46+
}
47+
48+
log_success!(
49+
"Successfully generated Ruby SDK in {}",
50+
output_dir.display().to_string().magenta()
51+
);
52+
Ok(())
53+
}
54+
55+
fn which_exists(cmd: &str) -> bool {
56+
Command::new("which")
57+
.arg(cmd)
58+
.output()
59+
.map(|o| o.status.success())
60+
.unwrap_or(false)
61+
}

xdk-config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ python = "0.9.0"
77

88
# TypeScript SDK version
99
typescript = "0.5.0"
10+
11+
# Ruby SDK version
12+
ruby = "0.1.1"

xdk-gen/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
///
3232
/// See the `python` module for a reference implementation of a language generator.
3333
pub use python::Python;
34+
pub use ruby::Ruby;
3435
pub use typescript::TypeScript;
3536

3637
mod python;
38+
mod ruby;
3739
mod typescript;

xdk-gen/src/ruby/generator.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/// Ruby SDK Generator Implementation
2+
///
3+
/// This file implements the Ruby generator using the `language!` macro.
4+
/// It defines filters for Ruby-specific formatting and implements the generator.
5+
use xdk_lib::{Casing, language, pascal_case};
6+
7+
/// MiniJinja filter for converting OpenAPI types to Ruby types
8+
fn ruby_type(value: &str) -> String {
9+
let ruby_type = match value {
10+
"string" => "String",
11+
"integer" => "Integer",
12+
"number" => "Float",
13+
"boolean" => "Boolean",
14+
"array" => "Array",
15+
"object" => "Hash",
16+
_ => "Object",
17+
};
18+
ruby_type.to_string()
19+
}
20+
21+
/// MiniJinja filter for getting the last part of a path (splits by both '/' and '.')
22+
fn last_part(value: &str) -> String {
23+
value
24+
.split('/')
25+
.next_back()
26+
.unwrap_or(value)
27+
.split('.')
28+
.next_back()
29+
.unwrap_or(value)
30+
.to_string()
31+
}
32+
33+
/// Helper function for snake_case conversion (for use as a filter)
34+
fn snake_case(value: &str) -> String {
35+
Casing::Snake.convert_string(value)
36+
}
37+
38+
/*
39+
This is the main generator for the Ruby SDK
40+
It declares the templates and filters used as well as the rendering logic
41+
*/
42+
language! {
43+
name: Ruby,
44+
filters: [pascal_case, snake_case, ruby_type, last_part],
45+
class_casing: Casing::Pascal,
46+
operation_casing: Casing::Snake,
47+
import_casing: Casing::Snake,
48+
variable_casing: Casing::Snake,
49+
render: [
50+
multiple {
51+
render "models" => "lib/xdk/{}/models.rb",
52+
render "client_class" => "lib/xdk/{}/client.rb"
53+
},
54+
render "main_client" => "lib/xdk/client.rb",
55+
render "version" => "lib/xdk/version.rb",
56+
render "lib_entry" => "lib/xdk.rb",
57+
render "gemspec" => "xdk.gemspec",
58+
render "gemfile" => "Gemfile",
59+
render "readme" => "README.md",
60+
render "gitignore" => ".gitignore"
61+
],
62+
tests: [
63+
multiple {
64+
render "test_contracts" => "spec/{}/contracts_spec.rb",
65+
render "test_structure" => "spec/{}/structure_spec.rb"
66+
},
67+
render "spec_helper" => "spec/spec_helper.rb"
68+
]
69+
}

xdk-gen/src/ruby/mod.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/// Ruby SDK Generator Module
2+
///
3+
/// This module implements the SDK generator for Ruby.
4+
mod generator;
5+
6+
pub use generator::Ruby;
7+
8+
#[cfg(test)]
9+
mod tests {
10+
use crate::ruby::generator::Ruby;
11+
use std::fs;
12+
use std::path::Path;
13+
use tempfile::Builder;
14+
use xdk_lib::Result;
15+
use xdk_lib::generator::generate;
16+
use xdk_openapi::{OpenApiContextGuard, parse_json_file};
17+
18+
fn create_output_dir() -> std::path::PathBuf {
19+
let temp_dir = Builder::new()
20+
.prefix("test_output_ruby")
21+
.tempdir()
22+
.expect("Failed to create temporary directory");
23+
temp_dir.path().to_path_buf()
24+
}
25+
26+
fn verify_sdk_structure(output_dir: &Path) {
27+
let lib_dir = output_dir.join("lib").join("xdk");
28+
assert!(lib_dir.exists(), "lib/xdk directory should exist");
29+
assert!(
30+
lib_dir.join("client.rb").exists(),
31+
"lib/xdk/client.rb should exist"
32+
);
33+
assert!(
34+
lib_dir.join("version.rb").exists(),
35+
"lib/xdk/version.rb should exist"
36+
);
37+
assert!(
38+
output_dir.join("xdk.gemspec").exists(),
39+
"xdk.gemspec should exist"
40+
);
41+
assert!(
42+
output_dir.join("README.md").exists(),
43+
"README.md should exist"
44+
);
45+
}
46+
47+
#[test]
48+
fn test_simple_openapi() {
49+
let output_dir = create_output_dir();
50+
let _guard = OpenApiContextGuard::new();
51+
let openapi = parse_json_file("../tests/openapi/simple.json").unwrap();
52+
let result = generate(Ruby, &openapi, &output_dir, "0.0.1-test");
53+
assert!(result.is_ok(), "Failed to generate SDK: {:?}", result);
54+
verify_sdk_structure(&output_dir);
55+
}
56+
57+
#[test]
58+
fn test_version_in_generated_files() {
59+
let output_dir = create_output_dir();
60+
let _guard = OpenApiContextGuard::new();
61+
let openapi = parse_json_file("../tests/openapi/simple.json").unwrap();
62+
63+
let test_version = "1.2.3-test";
64+
let result = generate(Ruby, &openapi, &output_dir, test_version);
65+
assert!(result.is_ok(), "Failed to generate SDK: {:?}", result);
66+
67+
let version_content = fs::read_to_string(output_dir.join("lib/xdk/version.rb"))
68+
.expect("Failed to read version.rb");
69+
assert!(
70+
version_content.contains("1.2.3-test"),
71+
"version.rb should contain the test version"
72+
);
73+
}
74+
}

0 commit comments

Comments
 (0)