Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# Generated SDKs (pushed to separate repos)
/xdk/python/
/xdk/typescript/
/xdk/ruby/

# Deploy keys (never commit)
/.keys/
Expand Down
18 changes: 13 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# XDK SDK Generator

.PHONY: all check build test clean help
.PHONY: generate python typescript
.PHONY: test-python test-typescript test-sdks
.PHONY: generate python typescript ruby
.PHONY: test-python test-typescript test-ruby test-sdks
.PHONY: fmt clippy test-generator
.PHONY: versions

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

generate: python typescript
generate: python typescript ruby

python:
cargo run -- python --latest true

typescript:
cargo run -- typescript --latest true

ruby:
cargo run -- ruby --latest true

# =====================================
# SDK Testing (local dev)
# =====================================
Expand All @@ -36,6 +39,9 @@ test-python: python
test-typescript: typescript
cd xdk/typescript && npm ci && npm run build && npm run type-check && npm test

test-ruby: ruby
cd xdk/ruby && bundle install && bundle exec rspec spec/ --format documentation

# =====================================
# Generator
# =====================================
Expand All @@ -61,14 +67,14 @@ test: test-generator test-sdks
# =====================================

versions:
@grep -E "^(python|typescript) = " xdk-config.toml
@grep -E "^(python|typescript|ruby) = " xdk-config.toml

# =====================================
# Cleanup
# =====================================

clean:
rm -rf xdk/python xdk/typescript
rm -rf xdk/python xdk/typescript xdk/ruby

cargo-clean:
cargo clean
Expand All @@ -82,8 +88,10 @@ help:
@echo ""
@echo "Local Development:"
@echo " make python Generate Python SDK"
@echo " make ruby Generate Ruby SDK"
@echo " make typescript Generate TypeScript SDK"
@echo " make test-python Generate + test Python SDK"
@echo " make test-ruby Generate + test Ruby SDK"
@echo " make test-typescript Generate + test TypeScript SDK"
@echo ""
@echo "Generator:"
Expand Down
59 changes: 59 additions & 0 deletions xdk-build/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// Declare modules
mod error;
mod python;
mod ruby;
mod typescript;
mod utils;

Expand Down Expand Up @@ -50,6 +51,19 @@ enum Commands {
#[arg(short, long, default_value = "xdk/typescript")]
output: PathBuf,
},
/// Generate a Ruby SDK from an OpenAPI specification
Ruby {
/// Path to the OpenAPI specification file
#[arg(short, long)]
spec: Option<PathBuf>,

#[arg(short, long)]
latest: Option<bool>,

/// Output directory for the generated SDK
#[arg(short, long, default_value = "xdk/ruby")]
output: PathBuf,
},
}

#[tokio::main]
Expand Down Expand Up @@ -164,6 +178,51 @@ async fn main() -> Result<()> {
// Call the generate method - `?` handles the Result conversion
typescript::generate(&openapi, &output)
}
Commands::Ruby {
spec,
output,
latest,
} => {
let openapi = if latest == Some(true) {
let client = reqwest::Client::new();
let response = client
.get("https://api.x.com/2/openapi.json")
.send()
.await
.map_err(|e| {
BuildError::CommandFailed(format!("Failed to fetch OpenAPI spec: {}", e))
})?;

let json_text = response.text().await.map_err(|e| {
BuildError::CommandFailed(format!("Failed to read response: {}", e))
})?;

parse_json(&json_text).map_err(|e| SdkGeneratorError::from(e.to_string()))?
} else {
let extension = spec
.as_ref()
.unwrap()
.extension()
.and_then(|ext| ext.to_str())
.ok_or_else(|| {
BuildError::CommandFailed("Invalid file extension".to_string())
})?;

match extension {
"yaml" | "yml" => parse_yaml_file(spec.as_ref().unwrap().to_str().unwrap())
.map_err(|e| SdkGeneratorError::from(e.to_string()))?,
"json" => parse_json_file(spec.as_ref().unwrap().to_str().unwrap())
.map_err(|e| SdkGeneratorError::from(e.to_string()))?,
_ => {
let err_msg = format!("Unsupported file extension: {}", extension);
return Err(BuildError::CommandFailed(err_msg));
}
}
};

log_info!("Specification parsed successfully.");
ruby::generate(&openapi, &output)
}
};

// Handle the result with better error messaging
Expand Down
61 changes: 61 additions & 0 deletions xdk-build/src/ruby.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::error::{BuildError, Result};
use colored::*;
use std::path::Path;
use std::process::Command;
use xdk_gen::Ruby;
use xdk_lib::{XdkConfig, log_error, log_info, log_success};
use xdk_openapi::OpenApi;

/// Generates the Ruby SDK.
pub fn generate(openapi: &OpenApi, output_dir: &Path) -> Result<()> {
log_info!("Generating Ruby SDK code...");

// Load configuration to get version
let config = XdkConfig::load_default().map_err(BuildError::SdkGenError)?;
let version = config.get_version("ruby").ok_or_else(|| {
BuildError::SdkGenError(xdk_lib::SdkGeneratorError::FrameworkError(
"Ruby version not found in config".to_string(),
))
})?;

// Create output directory if it doesn't exist
if let Err(e) = std::fs::create_dir_all(output_dir) {
log_error!(
"Failed to create output directory '{}': {}",
output_dir.display(),
e
);
return Err(BuildError::IoError(e));
}

// Generate the SDK code
if let Err(e) = xdk_lib::generator::generate(Ruby, openapi, output_dir, version) {
log_error!("Failed to generate Ruby SDK code: {}", e);
return Err(BuildError::SdkGenError(e));
}
log_success!("SDK code generated.");

// Optionally run rubocop for formatting (if available)
if which_exists("rubocop") {
log_info!("Formatting code with rubocop...");
let mut fmt_cmd = Command::new("rubocop");
fmt_cmd.arg("-a").arg(output_dir);
// Best-effort formatting; don't fail the build if rubocop isn't configured
let _ = fmt_cmd.output();
log_success!("Rubocop formatting complete.");
}

log_success!(
"Successfully generated Ruby SDK in {}",
output_dir.display().to_string().magenta()
);
Ok(())
}

fn which_exists(cmd: &str) -> bool {
Command::new("which")
.arg(cmd)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
3 changes: 3 additions & 0 deletions xdk-config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ python = "0.9.0"

# TypeScript SDK version
typescript = "0.5.0"

# Ruby SDK version
ruby = "0.1.1"
2 changes: 2 additions & 0 deletions xdk-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
///
/// See the `python` module for a reference implementation of a language generator.
pub use python::Python;
pub use ruby::Ruby;
pub use typescript::TypeScript;

mod python;
mod ruby;
mod typescript;
69 changes: 69 additions & 0 deletions xdk-gen/src/ruby/generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/// Ruby SDK Generator Implementation
///
/// This file implements the Ruby generator using the `language!` macro.
/// It defines filters for Ruby-specific formatting and implements the generator.
use xdk_lib::{Casing, language, pascal_case};

/// MiniJinja filter for converting OpenAPI types to Ruby types
fn ruby_type(value: &str) -> String {
let ruby_type = match value {
"string" => "String",
"integer" => "Integer",
"number" => "Float",
"boolean" => "Boolean",
"array" => "Array",
"object" => "Hash",
_ => "Object",
};
ruby_type.to_string()
}

/// MiniJinja filter for getting the last part of a path (splits by both '/' and '.')
fn last_part(value: &str) -> String {
value
.split('/')
.next_back()
.unwrap_or(value)
.split('.')
.next_back()
.unwrap_or(value)
.to_string()
}

/// Helper function for snake_case conversion (for use as a filter)
fn snake_case(value: &str) -> String {
Casing::Snake.convert_string(value)
}

/*
This is the main generator for the Ruby SDK
It declares the templates and filters used as well as the rendering logic
*/
language! {
name: Ruby,
filters: [pascal_case, snake_case, ruby_type, last_part],
class_casing: Casing::Pascal,
operation_casing: Casing::Snake,
import_casing: Casing::Snake,
variable_casing: Casing::Snake,
render: [
multiple {
render "models" => "lib/xdk/{}/models.rb",
render "client_class" => "lib/xdk/{}/client.rb"
},
render "main_client" => "lib/xdk/client.rb",
render "version" => "lib/xdk/version.rb",
render "lib_entry" => "lib/xdk.rb",
render "gemspec" => "xdk.gemspec",
render "gemfile" => "Gemfile",
render "readme" => "README.md",
render "gitignore" => ".gitignore"
],
tests: [
multiple {
render "test_contracts" => "spec/{}/contracts_spec.rb",
render "test_structure" => "spec/{}/structure_spec.rb"
},
render "spec_helper" => "spec/spec_helper.rb"
]
}
74 changes: 74 additions & 0 deletions xdk-gen/src/ruby/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/// Ruby SDK Generator Module
///
/// This module implements the SDK generator for Ruby.
mod generator;

pub use generator::Ruby;

#[cfg(test)]
mod tests {
use crate::ruby::generator::Ruby;
use std::fs;
use std::path::Path;
use tempfile::Builder;
use xdk_lib::Result;
use xdk_lib::generator::generate;
use xdk_openapi::{OpenApiContextGuard, parse_json_file};

fn create_output_dir() -> std::path::PathBuf {
let temp_dir = Builder::new()
.prefix("test_output_ruby")
.tempdir()
.expect("Failed to create temporary directory");
temp_dir.path().to_path_buf()
}

fn verify_sdk_structure(output_dir: &Path) {
let lib_dir = output_dir.join("lib").join("xdk");
assert!(lib_dir.exists(), "lib/xdk directory should exist");
assert!(
lib_dir.join("client.rb").exists(),
"lib/xdk/client.rb should exist"
);
assert!(
lib_dir.join("version.rb").exists(),
"lib/xdk/version.rb should exist"
);
assert!(
output_dir.join("xdk.gemspec").exists(),
"xdk.gemspec should exist"
);
assert!(
output_dir.join("README.md").exists(),
"README.md should exist"
);
}

#[test]
fn test_simple_openapi() {
let output_dir = create_output_dir();
let _guard = OpenApiContextGuard::new();
let openapi = parse_json_file("../tests/openapi/simple.json").unwrap();
let result = generate(Ruby, &openapi, &output_dir, "0.0.1-test");
assert!(result.is_ok(), "Failed to generate SDK: {:?}", result);
verify_sdk_structure(&output_dir);
}

#[test]
fn test_version_in_generated_files() {
let output_dir = create_output_dir();
let _guard = OpenApiContextGuard::new();
let openapi = parse_json_file("../tests/openapi/simple.json").unwrap();

let test_version = "1.2.3-test";
let result = generate(Ruby, &openapi, &output_dir, test_version);
assert!(result.is_ok(), "Failed to generate SDK: {:?}", result);

let version_content = fs::read_to_string(output_dir.join("lib/xdk/version.rb"))
.expect("Failed to read version.rb");
assert!(
version_content.contains("1.2.3-test"),
"version.rb should contain the test version"
);
}
}
Loading