-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
feat: Add intentional nil encoding strategy to URLEncodedFormEncoder #3977
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: master
Are you sure you want to change the base?
feat: Add intentional nil encoding strategy to URLEncodedFormEncoder #3977
Conversation
- Implement dual strategy pattern for nil encoding with separate behaviors for encode() and encodeIfPresent() - Add intentionalOnly strategy that encodes nil as 'null' for explicit encode() calls while skipping nil for encodeIfPresent() - Support custom dual strategies through new NilEncoding initializer with encode/encodeIfPresent parameters - Maintain backward compatibility with existing single-strategy API - Add comprehensive test coverage including array recursion, mixed scenarios, and edge cases - Enable JSONEncoder-compatible nil handling for form-encoded requests
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.
Thanks for the PR! This looks like a good approach, but you need to be more careful about source and behavioral changes before it can be merged. Please ensure no source or behavior changes are visible to consumers of the API. Additionally, you can consolidate the added tests into the existing encoder tests.
| public struct NilEncoding: Sendable { | ||
| /// Encodes `nil` by dropping the entire key / value pair. | ||
| public static let dropKey = NilEncoding { nil } | ||
| public static let dropKey = NilEncoding(encoding: { nil }) |
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.
Unfortunately this is a source breaking change and can't be required. Keeping the existing init around is a good first step, but you need to ensure there's no ambiguity between the old and new versions when using the unlabeled form.
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.
Sorry, may I ask what you mean by βambiguityβ?
- I havenβt modified the signature of the existing
initializer. - The new
initializertakes 2 parameters, while the existing one only takes 1. - Also, the parameter names are different.
The change here, adding parameter labels, I thought it will make more explicit and easier to understand, while keeping the same behavior as the original trailing closure.
| /// - encode: Strategy used when `encode(nil)` or `encodeNil()` is called directly | ||
| /// - encodeIfPresent: Strategy used when `encodeIfPresent(nil)` is called | ||
| public init( | ||
| encode: @escaping @Sendable () -> String?, |
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 first parameter could be encodeNil while the second remains encodeIfPresent, just to make it a bit more clear.
| private let encodeEncoding: @Sendable () -> String? | ||
| private let encodeIfPresentEncoding: @Sendable () -> String? | ||
|
|
||
| /// Creates a NilEncoding with separate strategies for encode and encodeIfPresent methods. |
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.
Please use tick marks ("`") for type and parameter names in docs.
| } else { | ||
| try encodeNil(forKey: key) | ||
| // Use the encodeIfPresent strategy for nil values | ||
| guard let nilValue = nilEncoding.encodeNilIfPresent() else { return } |
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.
This behavior can't change either. Instead it needs to default to the current behavior and only use the new behavior if the user provides the encodeIfPresent closure.
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.
Thank you for pointing this out.
From my perspective, the current behavior remains fully backward-compatible.
- When using the single-parameter initializer, the
encodeIfPresentEncodingandencodeEncodingclosures are equivalent. - Here calling
nilEncoding.encodeNilIfPresent()can fall back tonilEncoding.encodeNil()automatically.
Implementing it as you suggested would require to:
- changing
encodeIfPresentEncodingtooptional, and when using single-parameter initializer, set it asnil. - exposing
encodeIfPresentEncodingasfileprivateor introducing an additionalfileprivate(wider than private) flag to check fornil; otherwise, cannot directly unwrap it usingif let.
So, may I ask you whether the current implementation is acceptable? Or if you have any further concerns, please let me know.
| // | ||
| // IntentionalNilEncodingTests.swift | ||
| // | ||
| // Copyright (c) 2014-2018 Alamofire Software Foundation (http://alamofire.org/) |
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.
All new files should use the current year for copyright.
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.
Really, though, these tests should be consolidated into the existing encoding tests.
| final class IntentionalNilEncodingTests: BaseTestCase { | ||
| // MARK: - Test Helper Structures | ||
|
|
||
| struct TestStruct: Encodable { |
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.
Feel free to consolidate all of the test structs as fileprivates at the bottom of this file.
- Integrate nil encoding tests into existing ParameterEncoderTests following project patterns - Use backticks for proper variable and value documentation formatting - Rename parameters for better readability and understanding - Remove standalone test file in favor of consolidated test structure
Issue Link π
#3870
Goals β½
Enable different nil handling behaviors for
encode()vsencodeIfPresent()inURLEncodedFormEncoder. Adds.intentionalOnlystrategy that matches JSONEncoder semantics while maintaining full backward compatibility.Implementation Details π§
Core Changes:
NilEncodingwith dual strategy support viainit(encode:encodeIfPresent:)encodeNilIfPresent()method and updatedKeyedContainer._encodeIfPresent()to use it.intentionalOnlystrategy: encodes nil as "null" forencode(), skips forencodeIfPresent()Usage:
Backward compatibility: Existing
init(encoding:)API unchanged.Testing Details π
New Tests: 10 test cases in
IntentionalNilEncodingTests.intentionalOnlystrategy behaviorResults: All tests pass, existing URLEncodedFormEncoder tests unaffected.