-
Notifications
You must be signed in to change notification settings - Fork 5
feat(pay): add forward-compatible enum handling #376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| use std::{env, fs, path::PathBuf}; | ||
|
|
||
| fn main() { | ||
| println!("cargo:rerun-if-changed=src/pay/openapi.json"); | ||
|
|
||
| // Only generate pay API if the pay feature is enabled | ||
| if env::var("CARGO_FEATURE_PAY").is_ok() { | ||
| generate_pay_api(); | ||
| } | ||
| } | ||
|
|
||
| fn generate_pay_api() { | ||
| let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); | ||
| let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); | ||
| let spec_path = manifest_dir.join("src/pay/openapi.json"); | ||
|
|
||
| // Read and preprocess the OpenAPI spec as JSON | ||
| let spec_content = | ||
| fs::read_to_string(&spec_path).expect("Failed to read openapi.json"); | ||
| let mut spec: serde_json::Value = serde_json::from_str(&spec_content) | ||
| .expect("Failed to parse openapi.json"); | ||
|
|
||
| // Remove enum constraints from specific schemas to make them | ||
| // forward-compatible (unknown values won't fail deserialization) | ||
| remove_enum_constraint(&mut spec, "PaymentStatus"); | ||
| remove_enum_constraint(&mut spec, "CollectDataFieldType"); | ||
|
|
||
| // Write the preprocessed spec | ||
| let preprocessed_path = out_dir.join("pay_openapi_preprocessed.json"); | ||
| fs::write(&preprocessed_path, serde_json::to_string_pretty(&spec).unwrap()) | ||
| .expect("Failed to write preprocessed spec"); | ||
|
|
||
| // Parse preprocessed JSON as OpenAPI spec | ||
| let file = fs::File::open(&preprocessed_path).unwrap(); | ||
| let spec: openapiv3::OpenAPI = | ||
| serde_json::from_reader(file).expect("Failed to parse as OpenAPI"); | ||
|
|
||
| // Generate progenitor code with Builder interface and Separate tags | ||
| let mut settings = progenitor::GenerationSettings::default(); | ||
| settings.with_interface(progenitor::InterfaceStyle::Builder); | ||
| settings.with_tag(progenitor::TagStyle::Separate); | ||
| settings.with_derive("PartialEq".to_string()); | ||
| let mut generator = progenitor::Generator::new(&settings); | ||
| let tokens = generator | ||
| .generate_tokens(&spec) | ||
| .expect("Failed to generate progenitor tokens"); | ||
| let ast = syn::parse2(tokens).expect("Failed to parse generated tokens"); | ||
| let content = prettyplease::unparse(&ast); | ||
|
|
||
| let codegen_path = out_dir.join("pay_codegen.rs"); | ||
| fs::write(&codegen_path, content).expect("Failed to write generated code"); | ||
| } | ||
|
|
||
| fn remove_enum_constraint(spec: &mut serde_json::Value, schema_name: &str) { | ||
| if let Some(schema) = | ||
| spec.pointer_mut(&format!("/components/schemas/{}", schema_name)) | ||
| { | ||
| if let Some(obj) = schema.as_object_mut() { | ||
| obj.remove("enum"); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,7 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| progenitor::generate_api!( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| spec = "src/pay/openapi.json", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface = Builder, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tags = Separate, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| derives = [PartialEq], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Include progenitor-generated code from build.rs | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // The build script preprocesses openapi.json to remove enum constraints, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // making the API forward-compatible with new enum values. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| include!(concat!(env!("OUT_DIR"), "/pay_codegen.rs")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @@ -19,6 +19,29 @@ | ||
| }; | ||
| } | ||
|
|
||
| /// Redact sensitive secrets (such as API keys) before logging. | ||
| /// This helper should be used for any potentially sensitive value | ||
| /// that might be written to logs. | ||
| fn redact_secret<T: AsRef<str>>(secret: T) -> String { | ||
| let s = secret.as_ref(); | ||
| if s.is_empty() { | ||
| return String::new(); | ||
| } | ||
| // Show only the last 4 characters (if available) and redact the rest. | ||
| let suffix_len = 4usize.min(s.len()); | ||
| let suffix = &s[s.len() - suffix_len..]; | ||
| format!("***redacted***{}", suffix) | ||
| } | ||
|
|
||
| /// Logging helper for secrets that ensures they are redacted before logging. | ||
| /// Example usage: | ||
| /// pay_debug_secret!("Using API key: {}", redact_secret(api_key)); | ||
| macro_rules! pay_debug_secret { | ||
| ($($arg:tt)*) => { | ||
| tracing::debug!($($arg)*) | ||
| }; | ||
| } | ||
|
|
||
| macro_rules! pay_error { | ||
| ($($arg:tt)*) => { | ||
| tracing::error!($($arg)*) |
Check failure
Code scanning / CodeQL
Cleartext transmission of sensitive information High
builder.api_key(...)
This 'post' operation transmits data which may contain unencrypted sensitive data from
builder.api_key(...)
Copilot Autofix
AI about 2 months ago
Copilot could not generate an autofix suggestion
Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.
ganchoradkov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
ganchoradkov marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
Copilot
AI
Feb 5, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests verify that unknown enum values can be deserialized from the progenitor-generated types, but they don't test serialization or round-trip serialization/deserialization of the public-facing enum types. Consider adding tests that verify:
- Known variants serialize correctly:
PaymentStatus::Succeeded→"succeeded" - Unknown variants serialize correctly:
PaymentStatus::Unknown { value: "new_status" }→"new_status" - Unknown variants deserialize correctly from JSON:
"new_status"→PaymentStatus::Unknown { value: "new_status" } - Round-trip works: serialize → deserialize → equals original
This is especially important given that the JSON API in json.rs uses serde_json::to_string to serialize these types.
Uh oh!
There was an error while loading. Please reload this page.