binder: snapshot

This commit is contained in:
nym21
2025-12-21 00:42:54 +01:00
parent 5e3519aad4
commit 78a4d1af65
2 changed files with 167 additions and 39 deletions
+83 -19
View File
@@ -10,7 +10,7 @@ use serde_json::Value;
use crate::{ use crate::{
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
TypeSchemas, extract_inner_type, get_first_leaf_name, get_node_fields, TypeSchemas, extract_inner_type, get_first_leaf_name, get_node_fields,
get_pattern_instance_base, to_camel_case, to_pascal_case, unwrap_allof, get_pattern_instance_base, to_camel_case, to_pascal_case,
}; };
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints /// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints
@@ -100,10 +100,43 @@ fn is_primitive_alias(schema: &Value) -> bool {
&& schema.get("enum").is_none() && schema.get("enum").is_none()
} }
/// Convert a single JSON type string to JavaScript type
fn json_type_to_js(ty: &str, schema: &Value) -> String {
match ty {
"integer" | "number" => "number".to_string(),
"boolean" => "boolean".to_string(),
"string" => "string".to_string(),
"null" => "null".to_string(),
"array" => {
let item_type = schema
.get("items")
.map(schema_to_js_type)
.unwrap_or_else(|| "*".to_string());
format!("{}[]", item_type)
}
"object" => {
// Check if it has additionalProperties (dict-like)
if let Some(add_props) = schema.get("additionalProperties") {
let value_type = schema_to_js_type(add_props);
return format!("Object.<string, {}>", value_type);
}
"Object".to_string()
}
_ => "*".to_string(),
}
}
/// Convert JSON Schema to JavaScript/JSDoc type /// Convert JSON Schema to JavaScript/JSDoc type
fn schema_to_js_type(schema: &Value) -> String { fn schema_to_js_type(schema: &Value) -> String {
// Unwrap single-element allOf (schemars uses this for composition) // Handle allOf (try each element until we find a resolvable type)
let schema = unwrap_allof(schema); if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
for item in all_of {
let resolved = schema_to_js_type(item);
if resolved != "*" {
return resolved;
}
}
}
// Handle $ref // Handle $ref
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) { if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
@@ -122,23 +155,39 @@ fn schema_to_js_type(schema: &Value) -> String {
} }
} }
// Handle type field // Handle type field (can be string or array of strings)
if let Some(ty) = schema.get("type").and_then(|t| t.as_str()) { if let Some(ty) = schema.get("type") {
return match ty { // Handle array of types like ["string", "null"] for Optional
"integer" | "number" => "number".to_string(), if let Some(type_array) = ty.as_array() {
"boolean" => "boolean".to_string(), let types: Vec<String> = type_array
"string" => "string".to_string(), .iter()
"null" => "null".to_string(), .filter_map(|t| t.as_str())
"array" => { .filter(|t| *t != "null")
let item_type = schema .map(|t| json_type_to_js(t, schema))
.get("items") .collect();
.map(schema_to_js_type) let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
.unwrap_or_else(|| "*".to_string());
format!("{}[]", item_type) if types.len() == 1 {
let base_type = &types[0];
return if has_null {
format!("?{}", base_type)
} else {
base_type.clone()
};
} else if !types.is_empty() {
let union = format!("({})", types.join("|"));
return if has_null {
format!("?{}", union)
} else {
union
};
} }
"object" => "Object".to_string(), }
_ => "*".to_string(),
}; // Handle single type string
if let Some(ty_str) = ty.as_str() {
return json_type_to_js(ty_str, schema);
}
} }
// Handle anyOf/oneOf // Handle anyOf/oneOf
@@ -148,9 +197,24 @@ fn schema_to_js_type(schema: &Value) -> String {
.and_then(|v| v.as_array()) .and_then(|v| v.as_array())
{ {
let types: Vec<String> = variants.iter().map(schema_to_js_type).collect(); let types: Vec<String> = variants.iter().map(schema_to_js_type).collect();
// Filter out * and null for cleaner unions
let filtered: Vec<_> = types.iter().filter(|t| *t != "*").collect();
if !filtered.is_empty() {
return format!("({})", filtered.iter().map(|s| s.as_str()).collect::<Vec<_>>().join("|"));
}
return format!("({})", types.join("|")); return format!("({})", types.join("|"));
} }
// Check for format hint without type (common in OpenAPI)
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
return match format {
"int32" | "int64" => "number".to_string(),
"float" | "double" => "number".to_string(),
"date" | "date-time" => "string".to_string(),
_ => "*".to_string(),
};
}
"*".to_string() "*".to_string()
} }
+84 -20
View File
@@ -10,7 +10,7 @@ use serde_json::Value;
use crate::{ use crate::{
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
TypeSchemas, extract_inner_type, get_node_fields, get_pattern_instance_base, is_enum_schema, TypeSchemas, extract_inner_type, get_node_fields, get_pattern_instance_base, is_enum_schema,
to_pascal_case, to_snake_case, unwrap_allof, to_pascal_case, to_snake_case,
}; };
/// Generate Python client from metadata and OpenAPI endpoints /// Generate Python client from metadata and OpenAPI endpoints
@@ -187,10 +187,44 @@ fn collect_schema_refs(schema: &Value, refs: &mut std::collections::HashSet<Stri
} }
} }
/// Convert a single JSON type string to Python type
fn json_type_to_python(ty: &str, schema: &Value) -> String {
match ty {
"integer" => "int".to_string(),
"number" => "float".to_string(),
"boolean" => "bool".to_string(),
"string" => "str".to_string(),
"null" => "None".to_string(),
"array" => {
let item_type = schema
.get("items")
.map(schema_to_python_type)
.unwrap_or_else(|| "Any".to_string());
format!("List[{}]", item_type)
}
"object" => {
// Check if it has additionalProperties (dict-like)
if let Some(add_props) = schema.get("additionalProperties") {
let value_type = schema_to_python_type(add_props);
return format!("dict[str, {}]", value_type);
}
"dict".to_string()
}
_ => "Any".to_string(),
}
}
/// Convert JSON Schema to Python type /// Convert JSON Schema to Python type
fn schema_to_python_type(schema: &Value) -> String { fn schema_to_python_type(schema: &Value) -> String {
// Unwrap single-element allOf (schemars uses this for composition) // Handle allOf (try each element until we find a resolvable type)
let schema = unwrap_allof(schema); if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
for item in all_of {
let resolved = schema_to_python_type(item);
if resolved != "Any" {
return resolved;
}
}
}
// Handle $ref // Handle $ref
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) { if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
@@ -209,24 +243,39 @@ fn schema_to_python_type(schema: &Value) -> String {
} }
} }
// Handle type field // Handle type field (can be string or array of strings)
if let Some(ty) = schema.get("type").and_then(|t| t.as_str()) { if let Some(ty) = schema.get("type") {
return match ty { // Handle array of types like ["string", "null"] for Optional
"integer" => "int".to_string(), if let Some(type_array) = ty.as_array() {
"number" => "float".to_string(), let types: Vec<String> = type_array
"boolean" => "bool".to_string(), .iter()
"string" => "str".to_string(), .filter_map(|t| t.as_str())
"null" => "None".to_string(), .filter(|t| *t != "null") // Filter out null for cleaner Optional handling
"array" => { .map(|t| json_type_to_python(t, schema))
let item_type = schema .collect();
.get("items") let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
.map(schema_to_python_type)
.unwrap_or_else(|| "Any".to_string()); if types.len() == 1 {
format!("List[{}]", item_type) let base_type = &types[0];
return if has_null {
format!("Optional[{}]", base_type)
} else {
base_type.clone()
};
} else if !types.is_empty() {
let union = types.join(" | ");
return if has_null {
format!("Optional[{}]", union)
} else {
union
};
} }
"object" => "dict".to_string(), }
_ => "Any".to_string(),
}; // Handle single type string
if let Some(ty_str) = ty.as_str() {
return json_type_to_python(ty_str, schema);
}
} }
// Handle anyOf/oneOf // Handle anyOf/oneOf
@@ -236,9 +285,24 @@ fn schema_to_python_type(schema: &Value) -> String {
.and_then(|v| v.as_array()) .and_then(|v| v.as_array())
{ {
let types: Vec<String> = variants.iter().map(schema_to_python_type).collect(); let types: Vec<String> = variants.iter().map(schema_to_python_type).collect();
// Filter out Any and null for cleaner unions
let filtered: Vec<_> = types.iter().filter(|t| *t != "Any").collect();
if !filtered.is_empty() {
return filtered.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(" | ");
}
return types.join(" | "); return types.join(" | ");
} }
// Check for format hint without type (common in OpenAPI)
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
return match format {
"int32" | "int64" => "int".to_string(),
"float" | "double" => "float".to_string(),
"date" | "date-time" => "str".to_string(),
_ => "Any".to_string(),
};
}
"Any".to_string() "Any".to_string()
} }