Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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