Skip to content
Open
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/elixir/

# Deploy keys (never commit)
/.keys/
Expand Down
20 changes: 14 additions & 6 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 elixir
.PHONY: test-python test-typescript test-elixir test-sdks
.PHONY: fmt clippy test-generator
.PHONY: versions

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

generate: python typescript
generate: python typescript elixir

python:
cargo run -- python --latest true

typescript:
cargo run -- typescript --latest true

elixir:
cargo run -- elixir --latest true

# =====================================
# SDK Testing (local dev)
# =====================================

test-sdks: test-python test-typescript
test-sdks: test-python test-typescript test-elixir

test-python: python
cd xdk/python && uv sync && uv run pytest tests/ -v

test-typescript: typescript
cd xdk/typescript && npm ci && npm run build && npm run type-check && npm test

test-elixir: elixir
cd xdk/elixir && mix deps.get && mix test

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

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

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

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

cargo-clean:
cargo clean
Expand All @@ -85,6 +91,8 @@ help:
@echo " make typescript Generate TypeScript SDK"
@echo " make test-python Generate + test Python SDK"
@echo " make test-typescript Generate + test TypeScript SDK"
@echo " make elixir Generate Elixir SDK"
@echo " make test-elixir Generate + test Elixir SDK"
@echo ""
@echo "Generator:"
@echo " make check Run fmt + clippy"
Expand Down
33 changes: 33 additions & 0 deletions xdk-build/src/elixir.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use crate::error::{BuildError, Result};
use std::path::Path;
use xdk_gen::Elixir;
use xdk_lib::{XdkConfig, generate as generate_sdk, log_info, log_success};
use xdk_openapi::OpenApi;

pub fn generate(openapi: &OpenApi, output_dir: &Path) -> Result<()> {
log_info!("Generating Elixir SDK...");
std::fs::create_dir_all(output_dir).map_err(BuildError::IoError)?;

let config = XdkConfig::load_default().map_err(BuildError::SdkGenError)?;
let version = config.get_version("elixir").ok_or_else(|| {
BuildError::SdkGenError(xdk_lib::SdkGeneratorError::FrameworkError(
"Elixir version not found in config".to_string(),
))
})?;

generate_sdk(Elixir, openapi, output_dir, version).map_err(BuildError::SdkGenError)?;

log_info!("Formatting generated Elixir files...");
let status = std::process::Command::new("mix")
.arg("format")
.current_dir(output_dir)
.status();

match status {
Ok(s) if s.success() => log_success!("Elixir SDK formatted."),
_ => log_info!("Warning: mix format not available, skipping formatting"),
}

log_success!("Elixir SDK generated in {}", output_dir.display());
Ok(())
}
60 changes: 60 additions & 0 deletions xdk-build/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#![allow(unused_imports)]

// Declare modules
mod elixir;
mod error;
mod python;
mod typescript;
Expand Down Expand Up @@ -50,6 +51,19 @@ enum Commands {
#[arg(short, long, default_value = "xdk/typescript")]
output: PathBuf,
},
/// Generate an Elixir SDK from an OpenAPI specification
Elixir {
/// 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/elixir")]
output: PathBuf,
},
}

#[tokio::main]
Expand Down Expand Up @@ -164,6 +178,52 @@ async fn main() -> Result<()> {
// Call the generate method - `?` handles the Result conversion
typescript::generate(&openapi, &output)
}
Commands::Elixir {
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.");

elixir::generate(&openapi, &output)
}
};

// Handle the result with better error messaging
Expand Down
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.5.0"

# TypeScript SDK version
typescript = "0.4.0"

# Elixir SDK version
elixir = "1.0.0"
66 changes: 66 additions & 0 deletions xdk-gen/src/elixir/generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use xdk_lib::{Casing, language, pascal_case};

fn snake_case(value: &str) -> String {
Casing::Snake.convert_string(value)
}

fn elixir_type(value: &str) -> String {
match value {
"string" => "String.t()",
"integer" => "integer()",
"number" => "float()",
"boolean" => "boolean()",
"array" => "list()",
"object" => "map()",
_ => "String.t()",
}
.to_string()
}

fn last_part(value: &str) -> String {
value
.split('/')
.next_back()
.unwrap_or(value)
.split('.')
.next_back()
.unwrap_or(value)
.to_string()
}

fn schema_name_from_ref(path: &str) -> String {
if path.starts_with("#/components/schemas/") {
path.trim_start_matches("#/components/schemas/").to_string()
} else {
path.split('/').next_back().unwrap_or(path).to_string()
}
}

language! {
name: Elixir,
filters: [pascal_case, snake_case, elixir_type, last_part, schema_name_from_ref],
class_casing: Casing::Pascal,
operation_casing: Casing::Snake,
import_casing: Casing::Snake,
variable_casing: Casing::Snake,
render: [
multiple {
render "client_class" => "lib/xdk/{}.ex"
},
render "main_client" => "lib/xdk.ex",
render "errors" => "lib/xdk/errors.ex",
render "query" => "lib/xdk/query.ex",
render "streaming" => "lib/xdk/streaming.ex",
render "paginator" => "lib/xdk/paginator.ex",
render "mix_exs" => "mix.exs",
render "readme" => "README.md",
render "gitignore" => ".gitignore",
render "formatter" => ".formatter.exs"
],
tests: [
multiple {
render "test_structure" => "test/xdk/{}_test.exs"
},
render "test_helper" => "test/test_helper.exs"
]
}
73 changes: 73 additions & 0 deletions xdk-gen/src/elixir/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
mod generator;

pub use generator::Elixir;

#[cfg(test)]
mod tests {
use crate::elixir::generator::Elixir;
use std::fs;
use tempfile::Builder;
use xdk_lib::generator::generate;
use xdk_openapi::{OpenApiContextGuard, parse_json_file};

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

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

let result = generate(Elixir, &openapi, &output_dir, "0.1.0");
assert!(result.is_ok(), "Failed to generate SDK: {:?}", result);

assert!(output_dir.join("mix.exs").exists());
assert!(output_dir.join("lib/xdk.ex").exists());
assert!(output_dir.join("lib/xdk/errors.ex").exists());
}

#[test]
fn test_version_in_mix_exs() {
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(Elixir, &openapi, &output_dir, test_version);
assert!(result.is_ok(), "Failed to generate SDK: {:?}", result);

let mix_exs = fs::read_to_string(output_dir.join("mix.exs")).unwrap();
assert!(
mix_exs.contains("1.2.3-test"),
"mix.exs should contain version 1.2.3-test"
);
}

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

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

let client = fs::read_to_string(output_dir.join("lib/xdk.ex")).unwrap();
assert!(
client.contains("xdk-elixir/"),
"client should contain User-Agent prefix"
);
assert!(
client.contains("@version \"0.1.0\""),
"client should contain @version module attribute with version"
);
}
}
2 changes: 2 additions & 0 deletions xdk-gen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
/// # Example
///
/// See the `python` module for a reference implementation of a language generator.
pub use elixir::Elixir;
pub use python::Python;
pub use typescript::TypeScript;

mod elixir;
mod python;
mod typescript;
Loading