use super::{
ElixirOpenRpcGenerator, OpenRpcGenerator, PhpOpenRpcGenerator, PythonOpenRpcGenerator, RubyOpenRpcGenerator,
RustOpenRpcGenerator, TypeScriptOpenRpcGenerator,
};
use crate::codegen::openrpc::spec_parser::{
OpenRpcError, OpenRpcInfo, OpenRpcMethod, OpenRpcParam, OpenRpcResult, OpenRpcSpec, parse_openrpc_schema,
};
use crate::codegen::{TargetLanguage, quality::QualityValidator};
use serde_json::json;
use std::path::Path;
fn minimal_spec() -> OpenRpcSpec {
OpenRpcSpec {
openrpc: "1.3.2".to_string(),
info: OpenRpcInfo {
title: "Test API".to_string(),
version: "1.0.0".to_string(),
description: None,
contact: None,
license: None,
},
methods: vec![],
servers: vec![],
components: Default::default(),
}
}
fn user_service_spec() -> OpenRpcSpec {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/openrpc_schemas/user_service.json");
parse_openrpc_schema(&fixture).expect("user_service OpenRPC fixture should parse")
}
fn single_method_spec(name: &str) -> OpenRpcSpec {
let mut spec = minimal_spec();
spec.methods = vec![OpenRpcMethod {
name: name.to_string(),
summary: Some("Test method".to_string()),
description: Some("A test method for code generation".to_string()),
params: vec![],
result: OpenRpcResult {
name: "result".to_string(),
description: Some("The result".to_string()),
schema: json!({
"type": "object",
"properties": {
"message": { "type": "string" }
}
}),
},
errors: vec![],
examples: vec![],
tags: vec![],
}];
spec
}
fn spec_with_params(name: &str, param_count: usize) -> OpenRpcSpec {
let mut spec = single_method_spec(name);
spec.methods[0].params = (0..param_count)
.map(|i| OpenRpcParam {
name: format!("param{}", i),
description: Some(format!("Parameter {}", i)),
required: i == 0,
schema: json!({"type": "string"}),
})
.collect();
spec
}
#[allow(dead_code)]
fn spec_with_errors(name: &str) -> OpenRpcSpec {
let mut spec = single_method_spec(name);
spec.methods[0].errors = vec![
OpenRpcError {
code: -32700,
message: "Parse error".to_string(),
data: None,
},
OpenRpcError {
code: -32600,
message: "Invalid request".to_string(),
data: None,
},
];
spec
}
#[test]
fn test_python_generator_validates_with_quality_gates() {
let spec = single_method_spec("getUser");
let generator = PythonOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Python OpenRPC generation should succeed");
let report = QualityValidator::new(TargetLanguage::Python)
.validate_all(&output)
.expect("Python OpenRPC validation should run");
assert!(
report.is_valid(),
"generated Python OpenRPC code should validate cleanly: {report}"
);
}
#[test]
fn test_python_generator_imports_msgspec() {
let spec = single_method_spec("test_method");
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("import msgspec"),
"Generated code should import msgspec"
);
}
#[test]
fn test_python_generator_async_handler_signature() {
let spec = single_method_spec("test_method");
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("async def handle_test_method"),
"Handler should be async function"
);
assert!(
output.contains("async def handle_test_method() -> TestMethodResult"),
"Handler should return its generated result DTO"
);
}
#[test]
fn test_python_generator_error_handling() {
let spec = single_method_spec("test_method");
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("except Exception as e"),
"Generated code should have exception handling"
);
assert!(output.contains("-32601"), "Should have method not found error code");
assert!(output.contains("-32603"), "Should have internal error code");
}
#[test]
fn test_python_generator_method_router() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
];
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("if method_name == \"user.get\""),
"Router should check for user.get"
);
assert!(
output.contains("if method_name == \"user.create\""),
"Router should check for user.create"
);
assert!(
output.contains("await handle_user_get"),
"Router should call user.get handler"
);
assert!(
output.contains("await handle_user_create"),
"Router should call user.create handler"
);
}
#[test]
fn test_python_generator_validation_schemas() {
let spec = spec_with_params("validate.test", 2);
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("class ValidateTestParams(msgspec.Struct"),
"Should generate params DTO"
);
assert!(output.contains("param0: str"), "Should include first parameter");
assert!(
output.contains("param1: str | None = None"),
"Optional parameters should remain optional in the generated DTO"
);
}
#[test]
fn test_python_generator_preserves_msgspec_field_aliases() {
let mut spec = single_method_spec("user.getById");
spec.methods[0].params = vec![OpenRpcParam {
name: "userId".to_string(),
description: Some("User identifier".to_string()),
required: true,
schema: json!({
"type": "string",
"format": "uuid"
}),
}];
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("user_id: UUID = msgspec.field(name=\"userId\")"),
"Should emit a typed msgspec alias for camelCase wire names"
);
assert!(
!output.contains("user_id: str # UUID = msgspec.field(name=\"userId\")"),
"Type comments must not comment out msgspec alias definitions"
);
}
#[test]
fn test_python_generator_empty_spec() {
let spec = minimal_spec();
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("async def handle_jsonrpc_call"),
"Should generate router even with no methods"
);
assert!(
output.contains("Method not found"),
"Should return method not found for empty spec"
);
}
#[test]
fn test_python_generator_special_method_names() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get.by.id").methods[0].clone(),
single_method_spec("user-create").methods[0].clone(),
single_method_spec("user_update").methods[0].clone(),
];
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("handle_user_get_by_id"),
"Should convert dots to underscores"
);
assert!(
output.contains("user-create"),
"Should preserve hyphens in method name check"
);
assert!(
output.contains("user_update"),
"Should preserve underscores in method name check"
);
}
#[test]
fn test_python_generator_complex_param_schemas() {
let mut spec = single_method_spec("complex_method");
spec.methods[0].params = vec![OpenRpcParam {
name: "config".to_string(),
description: Some("Configuration object".to_string()),
required: true,
schema: json!({
"type": "object",
"properties": {
"nested": {
"type": "object",
"properties": {
"deep": {"type": "string"}
}
},
"list": {
"type": "array",
"items": {"type": "string"}
}
}
}),
}];
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("class ComplexMethodParamsConfig(msgspec.Struct, frozen=True):"),
"Should promote shaped nested objects to named structs"
);
assert!(
output.contains("config: ComplexMethodParamsConfig"),
"Should reference named nested structs from params DTOs"
);
}
#[test]
fn test_python_generator_resolves_component_refs_with_named_structs() {
let spec = user_service_spec();
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("\"\"\"JSON-RPC 2.0 handlers generated from OpenRPC specification."));
assert!(output.contains("from __future__ import annotations"));
assert!(output.contains("import msgspec"));
assert!(output.contains("class User(msgspec.Struct, frozen=True):"));
assert!(output.contains("class CreateUserInput(msgspec.Struct, frozen=True):"));
assert!(output.contains("user: CreateUserInput"));
assert!(output.contains("profile: UserProfile | None = None"));
assert!(output.contains("preferences: UserProfilePreferences | None = None"));
assert!(output.contains("birth_date: date | None = msgspec.field(default=None, name=\"birthDate\")"));
assert!(output.contains("-> User"));
assert!(output.contains("msgspec.to_builtins("));
}
#[test]
fn test_python_generator_user_service_fixture_validates_with_quality_gates() {
let spec = user_service_spec();
let generator = PythonOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Python OpenRPC generation should succeed for user service fixture");
let report = QualityValidator::new(TargetLanguage::Python)
.validate_all(&output)
.expect("Python OpenRPC validation should run");
assert!(
report.is_valid(),
"generated Python OpenRPC code for user_service fixture should validate cleanly: {report}"
);
}
#[test]
fn test_python_generator_user_api_example_validates_with_quality_gates() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-api.openrpc.json");
let spec = parse_openrpc_schema(&fixture).expect("example OpenRPC schema should parse");
let generator = PythonOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Python OpenRPC generation should succeed for example schema");
let report = QualityValidator::new(TargetLanguage::Python)
.validate_all(&output)
.expect("Python OpenRPC validation should run");
assert!(
report.is_valid(),
"generated Python OpenRPC code for example schema should validate cleanly: {report}"
);
}
#[test]
fn test_python_generator_executable_imports() {
let spec = single_method_spec("test");
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("\"\"\"JSON-RPC 2.0 handlers"), "Should have docstring");
assert!(output.contains("if __name__ == \"__main__\""), "Should have main block");
}
#[test]
fn test_rust_generator_emits_jsonrpc_router() {
let spec = spec_with_params("user.get", 1);
let generator = RustOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("pub struct UserGetParams"));
assert!(output.contains("pub async fn handle_user_get"));
assert!(output.contains("pub async fn handle_jsonrpc_call"));
assert!(output.contains("pub fn register_jsonrpc_route"));
}
#[test]
fn test_rust_generator_resolves_component_refs_and_semantic_formats() {
let spec = user_service_spec();
let generator = RustOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("pub struct User"));
assert!(output.contains("pub id: uuid::Uuid"));
assert!(output.contains("pub last_login_at: Option<chrono::DateTime<chrono::Utc>>"));
assert!(output.contains("pub created_at: Option<chrono::DateTime<chrono::Utc>>"));
assert!(output.contains("pub profile: Option<UserProfile>"));
assert!(output.contains("pub birth_date: Option<chrono::NaiveDate>"));
assert!(output.contains("pub type UsersGetByIdResult = User;"));
assert!(output.contains("pub user: CreateUserInput"));
assert!(!output.contains("pub type UsersGetByIdResult = Value;"));
}
#[test]
fn test_rust_generator_user_service_fixture_validates_with_quality_gates() {
let spec = user_service_spec();
let generator = RustOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Rust OpenRPC generation should succeed for user service fixture");
let report = QualityValidator::new(TargetLanguage::Rust)
.validate_all(&output)
.expect("Rust OpenRPC validation should run");
assert!(
report.is_valid(),
"generated Rust OpenRPC code for user_service fixture should validate cleanly: {report}"
);
}
#[test]
fn test_rust_generator_emits_named_nested_object_models_for_fixture_components() {
let spec = user_service_spec();
let generator = RustOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Rust OpenRPC generation should succeed for user service fixture");
assert!(
output.contains("pub struct UserProfileSocialLinks"),
"component nested object fields should become named structs instead of Value"
);
assert!(
output.contains("pub social_links: Option<UserProfileSocialLinks>"),
"component fields should reference the generated nested struct"
);
assert!(
output.contains("pub struct UserProfilePreferences"),
"multiple nested object fields should emit their own named structs"
);
assert!(
output.contains("pub preferences: Option<UserProfilePreferences>"),
"component fields should stay strongly typed"
);
}
#[test]
fn test_rust_generator_keeps_free_form_objects_as_value() {
let spec = user_service_spec();
let generator = RustOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Rust OpenRPC generation should succeed for user service fixture");
assert!(
output.contains("pub metadata: Option<Value>"),
"free-form additionalProperties objects should remain Value instead of inventing a fake struct"
);
}
#[test]
fn test_rust_generator_emits_named_inline_param_models_for_openrpc_example() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-api.openrpc.json");
let spec = parse_openrpc_schema(&fixture).expect("user-api OpenRPC example should parse");
let generator = RustOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Rust OpenRPC generation should succeed for inline-param example");
assert!(
output.contains("pub struct UserCreateParamsUserData"),
"required inline object params should emit named structs before they are referenced"
);
assert!(
output.contains("pub struct UserUpdateParamsUpdates"),
"multiple inline object params should each emit their own named structs"
);
assert!(
output.contains("pub struct UserListParamsOptions"),
"optional inline object params should also emit named structs"
);
}
#[test]
fn test_typescript_generator_zod_schemas() {
let spec = single_method_spec("test_method");
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("import { z } from \"zod\""), "Should import zod");
assert!(output.contains("z.object"), "Should use zod objects for schemas");
}
#[test]
fn test_typescript_generator_handler_types() {
let spec = single_method_spec("test_method");
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("async function"), "Handlers should be async functions");
assert!(output.contains("Promise"), "Handlers should return Promise types");
}
#[test]
fn test_typescript_generator_method_dispatch() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
];
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("if (method === \"user.get\")"),
"Should dispatch user.get"
);
assert!(
output.contains("if (method === \"user.create\")"),
"Should dispatch user.create"
);
}
#[test]
fn test_typescript_generator_error_codes() {
let spec = single_method_spec("test_method");
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("-32601"), "Should have method not found code");
assert!(output.contains("-32603"), "Should have internal error code");
assert!(
output.contains("jsonrpc") && output.contains("2.0"),
"Should use JSON-RPC 2.0 format"
);
}
#[test]
fn test_typescript_generator_async_handlers() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("method1").methods[0].clone(),
single_method_spec("method2").methods[0].clone(),
single_method_spec("method3").methods[0].clone(),
];
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
let async_count = output.matches("async function handle").count();
assert_eq!(
async_count, 4,
"Should have main handler + 3 individual async handler functions"
);
}
#[test]
fn test_typescript_generator_strict_typing() {
let spec = spec_with_params("test", 2);
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("z.infer"), "Should use zod.infer for type safety");
assert!(
output.matches("z.string()").count() > 0,
"Should use specific Zod types"
);
}
#[test]
fn test_typescript_generator_export_default() {
let spec = single_method_spec("test");
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("export async function"),
"Should export handler function"
);
}
#[test]
fn test_typescript_generator_compiles() {
let mut spec = minimal_spec();
spec.methods = vec![single_method_spec("getStatus").methods[0].clone()];
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.starts_with("#!/usr/bin/env node"), "Should have node shebang");
assert!(output.contains("/**"), "Should have JSDoc comments");
assert!(output.contains("type JSONRPCRequest"), "Should define JSON-RPC types");
assert!(output.contains("type JSONRPCResponse"), "Should define response types");
}
#[test]
fn test_typescript_generator_user_api_example_validates_with_quality_gates() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-api.openrpc.json");
let spec = parse_openrpc_schema(&fixture).expect("example OpenRPC schema should parse");
let generator = TypeScriptOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("TypeScript OpenRPC generation should succeed");
let report = QualityValidator::new(TargetLanguage::TypeScript)
.validate_all(&output)
.expect("TypeScript OpenRPC validation should run");
assert!(
report.is_valid(),
"generated TypeScript OpenRPC example code should validate cleanly: {report}"
);
}
#[test]
fn test_typescript_generator_preserves_shaped_objects_and_optionality() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-api.openrpc.json");
let spec = parse_openrpc_schema(&fixture).expect("example OpenRPC schema should parse");
let generator = TypeScriptOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("TypeScript OpenRPC generation should succeed for example schema");
assert!(output.contains("userData: z.object({"));
assert!(output.contains("role: z.enum([\"user\", \"admin\"]).optional()"));
assert!(output.contains("options: z.object({"));
assert!(output.contains("users: z.array(z.object({"));
assert!(output.contains("pagination: z.object({"));
}
#[test]
fn test_typescript_generator_uses_unimplemented_stub_instead_of_any_result() {
let spec = single_method_spec("user.create");
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("throw new Error(\"TODO: Implement JSON-RPC method logic\");"));
assert!(!output.contains("return result as any"));
}
#[test]
fn test_php_generator_declare_strict_types() {
let spec = single_method_spec("test_method");
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("<?php"), "Should start with PHP opening tag");
let lines: Vec<&str> = output.lines().collect();
let strict_found = lines.iter().any(|line| line.contains("declare(strict_types=1)"));
assert!(strict_found, "Should declare strict types");
}
#[test]
fn test_php_generator_handler_classes() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
];
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("final class HandleUserGet"),
"Should generate user.get handler class"
);
assert!(
output.contains("final class HandleUserCreate"),
"Should generate user.create handler class"
);
}
#[test]
fn test_php_generator_execute_method() {
let spec = single_method_spec("test_method");
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("public function execute(array $params): array"),
"Handler should have execute method"
);
}
#[test]
fn test_php_generator_registry_pattern() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
];
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("final class HandlerRegistry"),
"Should have HandlerRegistry class"
);
assert!(
output.contains("public static function register()"),
"Should have register method"
);
assert!(
output.contains("public static function handle"),
"Should have handle method"
);
assert!(output.contains("self::$handlers"), "Should use handlers registry");
}
#[test]
fn test_php_generator_error_responses() {
let spec = single_method_spec("test_method");
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("-32601"), "Should have method not found error");
assert!(output.contains("-32603"), "Should have internal error");
assert!(
output.contains("'jsonrpc' => '2.0'"),
"Should format JSON-RPC responses"
);
assert!(output.contains("'error'"), "Error responses should have error field");
}
#[test]
fn test_php_generator_namespace_declaration() {
let spec = single_method_spec("test");
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("namespace JsonRpc\\Handlers"),
"Should declare namespace"
);
}
#[test]
fn test_php_generator_return_types() {
let spec = single_method_spec("test_method");
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains(": array"), "Handler should have array return type");
assert!(
output.contains("@param array{} $params") || output.contains("@param array{"),
"Handler should document structured params"
);
}
#[test]
fn test_php_generator_validates_with_quality_gates() {
let mut spec = minimal_spec();
spec.methods = vec![OpenRpcMethod {
name: "widget.create".to_string(),
summary: Some("Create a widget".to_string()),
description: None,
params: vec![
OpenRpcParam {
name: "name".to_string(),
description: None,
required: true,
schema: json!({"type": "string"}),
},
OpenRpcParam {
name: "count".to_string(),
description: None,
required: false,
schema: json!({"type": "integer"}),
},
],
result: OpenRpcResult {
name: "widget".to_string(),
description: None,
schema: json!({
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "name"]
}),
},
errors: vec![],
examples: vec![],
tags: vec![],
}];
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
let report = QualityValidator::new(TargetLanguage::Php)
.validate_all(&output)
.expect("php openrpc validation should run");
assert!(
report.is_valid(),
"generated PHP OpenRPC code should validate cleanly: {report}"
);
}
#[test]
fn test_php_generator_valid_syntax() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("test1").methods[0].clone(),
single_method_spec("test2").methods[0].clone(),
];
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.starts_with("<?php"), "Should start with PHP tag");
assert!(output.contains("namespace"), "Should have namespace");
assert!(output.contains("class"), "Should have classes");
let open_braces = output.matches('{').count();
let close_braces = output.matches('}').count();
assert_eq!(open_braces, close_braces, "Braces should be balanced");
}
#[test]
fn test_ruby_generator_module_definition() {
let mut spec = minimal_spec();
spec.methods = vec![single_method_spec("user.get").methods[0].clone()];
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("class HandleUserGet"), "Should define handler class");
}
#[test]
fn test_ruby_generator_method_signature() {
let spec = single_method_spec("test_method");
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("def execute(params)"),
"Handler should have execute method"
);
}
#[test]
fn test_ruby_generator_error_handling() {
let spec = single_method_spec("test_method");
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("rescue StandardError"), "Should handle errors");
assert!(
output.contains("-32601") || output.contains("-32_601"),
"Should have method not found error"
);
assert!(
output.contains("-32603") || output.contains("-32_603"),
"Should have internal error code"
);
}
#[test]
fn test_ruby_generator_handler_registry() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
];
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("HANDLERS = {"), "Should define handlers hash");
assert!(
output.contains("\"user.get\"") || output.contains("'user.get'"),
"Should register user.get"
);
assert!(
output.contains("\"user.create\"") || output.contains("'user.create'"),
"Should register user.create"
);
assert!(output.contains("class JsonRpcRouter"), "Should have router class");
assert!(
output.contains("def self.handle_call"),
"Should have handle_call method"
);
}
#[test]
fn test_ruby_generator_symbol_method_names() {
let spec = single_method_spec("test_method");
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("jsonrpc:") || output.contains("\"jsonrpc\""),
"Should use symbol or string keys"
);
}
#[test]
fn test_ruby_generator_valid_syntax() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("method1").methods[0].clone(),
single_method_spec("method2").methods[0].clone(),
];
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.starts_with("# frozen_string_literal: true"),
"Should start with frozen string literal pragma"
);
assert!(output.contains("require"), "Should have requires");
assert!(output.contains("class"), "Should have classes");
let open_braces = output.matches('{').count();
let close_braces = output.matches('}').count();
assert_eq!(open_braces, close_braces, "Braces should be balanced");
}
#[test]
fn test_ruby_generator_validates_with_quality_gates() {
let mut spec = minimal_spec();
spec.methods = vec![OpenRpcMethod {
name: "widget.create".to_string(),
summary: Some("Create a widget".to_string()),
description: None,
params: vec![
OpenRpcParam {
name: "name".to_string(),
description: None,
required: true,
schema: json!({"type": "string"}),
},
OpenRpcParam {
name: "count".to_string(),
description: None,
required: false,
schema: json!({"type": "integer"}),
},
],
result: OpenRpcResult {
name: "widget".to_string(),
description: None,
schema: json!({
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "name"]
}),
},
errors: vec![],
examples: vec![],
tags: vec![],
}];
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
let report = QualityValidator::new(TargetLanguage::Ruby)
.validate_all(&output)
.expect("ruby openrpc validation should run");
assert!(
report.is_valid(),
"generated Ruby OpenRPC code should validate cleanly: {report}"
);
}
#[test]
fn test_ruby_generator_user_api_example_validates_with_quality_gates() {
let fixture = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../testing_data/schemas/user-api.openrpc.json");
let spec = parse_openrpc_schema(&fixture).expect("example OpenRPC schema should parse");
let generator = RubyOpenRpcGenerator;
let output = generator
.generate_handler_app(&spec)
.expect("Ruby OpenRPC generation should succeed for example schema");
let report = QualityValidator::new(TargetLanguage::Ruby)
.validate_all(&output)
.expect("Ruby OpenRPC validation should run");
assert!(
report.is_valid(),
"generated Ruby OpenRPC code for example schema should validate cleanly: {report}"
);
}
#[test]
fn test_elixir_generator_uses_spikard_router() {
let spec = single_method_spec("user.get");
let generator = ElixirOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("use Spikard.Router"), "Should use Spikard.Router");
assert!(output.contains("post(\"/rpc\""), "Should expose /rpc endpoint");
assert!(
output.contains("defmodule TestApiJsonRpc.Handlers"),
"Should generate handlers module"
);
assert!(
output.contains("def handle_user_get(params)"),
"Should generate per-method handler"
);
}
#[test]
fn test_elixir_generator_dispatches_methods() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
];
let generator = ElixirOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("\"user.get\" ->"), "Should dispatch user.get");
assert!(output.contains("\"user.create\" ->"), "Should dispatch user.create");
assert!(
output.contains("error_response(-32601, \"Method not found\", request_id)"),
"Should return JSON-RPC method not found"
);
}
#[test]
fn test_elixir_generator_validates_with_quality_gates() {
let mut spec = minimal_spec();
spec.methods = vec![OpenRpcMethod {
name: "widget.create".to_string(),
summary: Some("Create a widget".to_string()),
description: None,
params: vec![
OpenRpcParam {
name: "name".to_string(),
description: None,
required: true,
schema: json!({"type": "string"}),
},
OpenRpcParam {
name: "count".to_string(),
description: None,
required: false,
schema: json!({"type": "integer"}),
},
],
result: OpenRpcResult {
name: "widget".to_string(),
description: None,
schema: json!({
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "name"]
}),
},
errors: vec![],
examples: vec![],
tags: vec![],
}];
let generator = ElixirOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
let report = QualityValidator::new(TargetLanguage::Elixir)
.validate_all(&output)
.expect("elixir openrpc validation should run");
assert!(
report.is_valid(),
"generated Elixir OpenRPC code should validate cleanly: {report}"
);
}
#[test]
fn test_elixir_generator_resolves_component_refs_and_preserves_json_keys() {
let spec = user_service_spec();
let generator = ElixirOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("defmodule UserManagementRpcApiJsonRpc.Types"));
assert!(output.contains("@type user :: %{"));
assert!(output.contains("required(:id) => String.t()") || output.contains("required(:\"id\") => String.t()"));
assert!(output.contains("@type users_get_by_id_result :: Types.user()"));
assert!(
output.contains("@type users_create_params :: %{required(:user) => Types.create_user_input()}")
|| output.contains("@type users_create_params :: %{required(:\"user\") => Types.create_user_input()}")
);
assert!(!output.contains("@type users_get_by_id_result :: term()"));
}
#[test]
fn test_elixir_generator_omits_unused_types_alias_for_inline_methods() {
let mut spec = minimal_spec();
spec.components.schemas.insert(
"UnusedWidget".to_string(),
json!({
"type": "object",
"properties": {
"id": { "type": "string" }
}
}),
);
spec.methods = vec![OpenRpcMethod {
name: "widget.create".to_string(),
summary: Some("Create a widget".to_string()),
description: None,
params: vec![OpenRpcParam {
name: "name".to_string(),
description: None,
required: true,
schema: json!({ "type": "string" }),
}],
result: OpenRpcResult {
name: "widget".to_string(),
description: None,
schema: json!({
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
},
"required": ["id", "name"]
}),
},
errors: vec![],
examples: vec![],
tags: vec![],
}];
let generator = ElixirOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
!output.contains("alias TestApiJsonRpc.Types, as: Types"),
"Inline-only methods should not emit an unused Types alias"
);
let report = QualityValidator::new(TargetLanguage::Elixir)
.validate_all(&output)
.expect("Elixir OpenRPC validation should run");
assert!(
report.is_valid(),
"generated Elixir OpenRPC code with unused component schemas should validate cleanly: {report}"
);
}
#[test]
fn test_generators_same_spec_produces_equivalent_behavior() {
let mut spec = minimal_spec();
spec.methods = vec![single_method_spec("test_method").methods[0].clone()];
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let elixir_gen = ElixirOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
let elixir_out = elixir_gen.generate_handler_app(&spec).unwrap();
assert!(!py_out.is_empty(), "Python should generate output");
assert!(!ts_out.is_empty(), "TypeScript should generate output");
assert!(!php_out.is_empty(), "PHP should generate output");
assert!(!ruby_out.is_empty(), "Ruby should generate output");
assert!(!elixir_out.is_empty(), "Elixir should generate output");
}
#[test]
fn test_generators_error_codes_consistent() {
let spec = single_method_spec("test_method");
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let elixir_gen = ElixirOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
let elixir_out = elixir_gen.generate_handler_app(&spec).unwrap();
for output in &[&py_out, &ts_out, &php_out, &ruby_out, &elixir_out] {
assert!(
output.contains("-32601") || output.contains("-32_601"),
"All should have -32601 (method not found)"
);
assert!(
output.contains("-32603") || output.contains("-32_603"),
"All should have -32603 (internal error)"
);
}
}
#[test]
fn test_generators_method_dispatch_correct_method() {
let mut spec = minimal_spec();
spec.methods = vec![
single_method_spec("user.get").methods[0].clone(),
single_method_spec("user.create").methods[0].clone(),
single_method_spec("user.delete").methods[0].clone(),
];
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let elixir_gen = ElixirOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
let elixir_out = elixir_gen.generate_handler_app(&spec).unwrap();
for (output, lang) in &[
(&py_out, "Python"),
(&ts_out, "TypeScript"),
(&php_out, "PHP"),
(&ruby_out, "Ruby"),
(&elixir_out, "Elixir"),
] {
assert!(output.contains("user.get"), "{} should handle user.get", lang);
assert!(output.contains("user.create"), "{} should handle user.create", lang);
assert!(output.contains("user.delete"), "{} should handle user.delete", lang);
}
}
#[test]
fn test_generators_parameter_validation_consistent() {
let spec = spec_with_params("validate_test", 2);
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let elixir_gen = ElixirOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
let elixir_out = elixir_gen.generate_handler_app(&spec).unwrap();
assert!(
py_out.contains("param0") || py_out.contains("Params"),
"Python should handle params"
);
assert!(
ts_out.contains("param0") || ts_out.contains("Schema"),
"TypeScript should handle params"
);
assert!(
php_out.contains("param0") || php_out.contains("$params"),
"PHP should handle params"
);
assert!(
ruby_out.contains("param0") || ruby_out.contains("params"),
"Ruby should handle params"
);
assert!(
elixir_out.contains("param0") || elixir_out.contains("params"),
"Elixir should handle params"
);
}
#[test]
fn test_generators_response_structure_identical() {
let spec = single_method_spec("test_method");
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let elixir_gen = ElixirOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
let elixir_out = elixir_gen.generate_handler_app(&spec).unwrap();
for (output, lang) in &[
(&py_out, "Python"),
(&ts_out, "TypeScript"),
(&php_out, "PHP"),
(&ruby_out, "Ruby"),
(&elixir_out, "Elixir"),
] {
assert!(
output.contains("jsonrpc") && (output.contains("result") || output.contains("error")),
"{} should have proper JSON-RPC structure",
lang
);
}
}
#[test]
fn test_generator_with_many_methods() {
let mut spec = minimal_spec();
for i in 0..10 {
spec.methods
.push(single_method_spec(&format!("method{}", i)).methods[0].clone());
}
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let elixir_gen = ElixirOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
let elixir_out = elixir_gen.generate_handler_app(&spec).unwrap();
assert!(py_out.matches("def handle_method").count() >= 10);
assert!(ts_out.matches("async function handle").count() >= 10);
assert!(php_out.matches("final class Handle").count() >= 10);
assert!(ruby_out.matches("class Handle").count() >= 10);
assert!(elixir_out.matches("def handle_method").count() >= 10);
}
#[test]
fn test_generator_with_complex_result_schemas() {
let mut spec = single_method_spec("complex");
spec.methods[0].result.schema = json!({
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"},
"email": {"type": "string", "format": "email"},
"tags": {
"type": "array",
"items": {"type": "string"}
}
}
},
"metadata": {
"type": "object",
"properties": {
"created_at": {"type": "string", "format": "date-time"},
"version": {"type": "integer"}
}
}
}
});
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
assert!(py_gen.generate_handler_app(&spec).is_ok());
assert!(ts_gen.generate_handler_app(&spec).is_ok());
assert!(php_gen.generate_handler_app(&spec).is_ok());
assert!(ruby_gen.generate_handler_app(&spec).is_ok());
}
#[test]
fn test_generator_with_array_result_schema() {
let mut spec = single_method_spec("list_items");
spec.methods[0].result.schema = json!({
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"name": {"type": "string"}
}
}
});
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
assert!(py_gen.generate_handler_app(&spec).is_ok());
assert!(ts_gen.generate_handler_app(&spec).is_ok());
assert!(php_gen.generate_handler_app(&spec).is_ok());
assert!(ruby_gen.generate_handler_app(&spec).is_ok());
}
#[test]
fn test_generator_with_various_param_types() {
let mut spec = single_method_spec("various_types");
spec.methods[0].params = vec![
OpenRpcParam {
name: "string_param".to_string(),
description: None,
required: true,
schema: json!({"type": "string"}),
},
OpenRpcParam {
name: "number_param".to_string(),
description: None,
required: true,
schema: json!({"type": "number"}),
},
OpenRpcParam {
name: "integer_param".to_string(),
description: None,
required: true,
schema: json!({"type": "integer"}),
},
OpenRpcParam {
name: "boolean_param".to_string(),
description: None,
required: false,
schema: json!({"type": "boolean"}),
},
OpenRpcParam {
name: "array_param".to_string(),
description: None,
required: false,
schema: json!({"type": "array", "items": {"type": "string"}}),
},
];
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
assert!(py_gen.generate_handler_app(&spec).is_ok());
assert!(ts_gen.generate_handler_app(&spec).is_ok());
assert!(php_gen.generate_handler_app(&spec).is_ok());
assert!(ruby_gen.generate_handler_app(&spec).is_ok());
}
#[test]
fn test_python_generator_frozen_struct() {
let spec = spec_with_params("frozen_test", 1);
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("frozen=True"), "Python DTOs should be frozen");
}
#[test]
fn test_typescript_generator_params_parsing() {
let spec = spec_with_params("parse_test", 2);
let generator = TypeScriptOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains(".parse(params)"),
"TypeScript should parse params with Zod"
);
}
#[test]
fn test_php_generator_param_validation_method() {
let spec = spec_with_params("validated", 2);
let generator = PhpOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("validateParams"), "PHP should have validation method");
assert!(
output.contains("private function validateParams"),
"Validation should be private"
);
}
#[test]
fn test_ruby_generator_param_validation_method() {
let spec = spec_with_params("validated", 2);
let generator = RubyOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(output.contains("validate_params"), "Ruby should have validation method");
assert!(output.contains("private"), "Validation should be private");
}
#[test]
fn test_all_generators_callable_by_trait() {
let spec = single_method_spec("test");
let generators: Vec<&dyn OpenRpcGenerator> = vec![
&PythonOpenRpcGenerator,
&TypeScriptOpenRpcGenerator,
&PhpOpenRpcGenerator,
&RubyOpenRpcGenerator,
];
for generator in generators {
assert!(
generator.generate_handler_app(&spec).is_ok(),
"All generators should work via trait"
);
}
}
#[test]
fn test_generator_language_names() {
assert_eq!(PythonOpenRpcGenerator.language_name(), "python");
assert_eq!(TypeScriptOpenRpcGenerator.language_name(), "typescript");
assert_eq!(PhpOpenRpcGenerator.language_name(), "php");
assert_eq!(RubyOpenRpcGenerator.language_name(), "ruby");
}
#[test]
fn test_generator_handles_empty_method_params() {
let spec = single_method_spec("no_params");
let generator = PythonOpenRpcGenerator;
let output = generator.generate_handler_app(&spec).unwrap();
assert!(
output.contains("async def handle_no_params"),
"Should generate handler without params"
);
}
#[test]
fn test_generator_handles_method_with_dots_in_name() {
let mut spec = minimal_spec();
spec.methods = vec![single_method_spec("namespace.subspace.method").methods[0].clone()];
let py_gen = PythonOpenRpcGenerator;
let ts_gen = TypeScriptOpenRpcGenerator;
let php_gen = PhpOpenRpcGenerator;
let ruby_gen = RubyOpenRpcGenerator;
let py_out = py_gen.generate_handler_app(&spec).unwrap();
let ts_out = ts_gen.generate_handler_app(&spec).unwrap();
let php_out = php_gen.generate_handler_app(&spec).unwrap();
let ruby_out = ruby_gen.generate_handler_app(&spec).unwrap();
assert!(py_out.contains("namespace.subspace.method"));
assert!(ts_out.contains("namespace.subspace.method"));
assert!(php_out.contains("namespace.subspace.method"));
assert!(ruby_out.contains("namespace.subspace.method"));
}