-
Notifications
You must be signed in to change notification settings - Fork 109
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
[Custom Fields] Differentiate between raw JSON values and String values that contain valid JSON #14292
base: trunk
Are you sure you want to change the base?
Conversation
Generated by 🚫 Danger |
country: CopiableProp<String> = .copy, | ||
country: NullableCopiableProp<String> = .copy, |
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.
I think the file wasn't re-generated after some changes in a previous PR, that's why rage generate
updated it.
📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.
|
Also ensures we can't bypass the validation by instantiating the enum case directly.
432aa36
to
4f82a10
Compare
return String(data: data, encoding: .utf8) ?? "" | ||
} else { | ||
return "\(value)" | ||
public struct MetaDataValue: Codable, Equatable, Sendable { |
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.
We expose a struct
instead of exposing the enum directly just to make sure we have some validation, and we no consumer can bypass it by instantiating the enum directly, ie: .json("invalid json")
, this is actually because as far as I found, Swift doesn't allow to have private initializer for the enum cases (unlike Kotlin with its sealed interface
feature).
private extension MetaDataValue { | ||
private enum ValueHolder: Codable, Equatable { | ||
case string(_ value: String) | ||
case json(_ json: 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.
I played with a different approach here, instead of making the backing data a String
, we can have it as Codable
directly, I don't have a strong opinion on which approach is best, so I kept the String
based implementation.
public struct MetaDataValue: Codable, Equatable, Sendable {
private let valueHolder: ValueHolder
public var stringValue: String {
valueHolder.stringValue
}
public var rawValue: String {
valueHolder.rawValue
}
public var isJson: Bool {
switch valueHolder {
case .string:
return false
case .json:
return true
}
}
public init(rawValue: String) {
let data = Data(rawValue.utf8)
if let jsonObject = try? JSONDecoder().decode(AnyCodable.self, from: data) {
valueHolder = try! ValueHolder(from: jsonObject.value)
} else {
valueHolder = .string(rawValue.removingPrefix("\"").removingSuffix("\""))
}
}
public init(from decoder: Decoder) throws {
valueHolder = try ValueHolder(from: decoder)
}
public func encode(to encoder: Encoder) throws {
try valueHolder.encode(to: encoder)
}
}
private extension MetaDataValue {
private enum ValueHolder: Codable, Equatable {
case string(_ value: String)
case json(_ json: Codable)
var stringValue: String {
switch self {
case .string(let value):
return value
case .json(let json):
return json.asJSONString()
}
}
var rawValue: String {
switch self {
case .string(let value):
return "\"\(value)\""
case .json(let json):
return json.asJSONString()
}
}
init(from decoder: any Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(AnyCodable.self)
try self.init(from: value.value)
}
init(from object: Any) throws {
switch object {
case let value as String:
self = .string(value)
case is Bool:
self = .string(String(describing: object))
case is any Numeric:
self = .string(String(describing: object))
default:
self = .json(AnyCodable(object))
}
}
func encode(to encoder: Encoder) throws {
switch self {
case .string(let value):
try value.encode(to: encoder)
case .json(let json):
try json.encode(to: encoder)
}
}
static func == (lhs: ValueHolder, rhs: ValueHolder) -> Bool {
return lhs.stringValue == rhs.stringValue
}
}
}
private extension Encodable {
func asJSONString() -> String {
if let data = try? JSONEncoder().encode(self) {
return String(data: data, encoding: .utf8) ?? ""
}
return ""
}
}
Closes: #14271
@hafizrahman @selanthiraiyan not urgent, please review when you have some free time.
Description
As detailed in the above issue, the current implementation in iOS doesn't allow to differentiate between JSON values and String values that contain valid JSON, and this causes a difference with how Android and Core behaves.
This PR updates the MetaData parsing to expose information on whether the value is a String or a Json using an
enum
, and we store this information also when caching: the logic here is simple, we store the information as it's in the response,Strings
will be stored with their quotes, and JSON values will be stored as they are without quotes. (the implementation is similar to what we did in Android, but simpler here)While the number of affected files may seem big, the changes are quite simple actually:
stringValue
, this returns the same content as before.rawValue
, this makes sure that String values are stored with theirquotes
.Note: While the impact of these changes could be small for this specific feature, I think it could have some benefits in the future, for example if we move Subscriptions or Addons to use the
MetaData
class directly, then we'll have to make sure we can use JSON data with it.Steps to reproduce
{"key":"value"}
Testing information
Screenshots
Simulator.Screen.Recording.-.iPhone.15.-.2024-11-01.at.18.21.44.mp4
RELEASE-NOTES.txt
if necessary.Reviewer (or Author, in the case of optional code reviews):
Please make sure these conditions are met before approving the PR, or request changes if the PR needs improvement: