-
Notifications
You must be signed in to change notification settings - Fork 121
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
SDL 0219 - Explicit returned type from NSDictionary(Store) category #685
Comments
This proposal doesn't really state how this change will solve the problem. Reading the code, the new method that all the RPC getters will use is: - (nullable id)sdl_objectForName:(SDLName)name ofClass:(Class)classType {
NSObject *obj = [self sdl_objectForName:name];
if (obj == nil || [obj isKindOfClass:classType]) {
return obj;
} else {
return [[classType alloc] initWithDictionary:(NSDictionary *)obj];
}
} But this means that if something is not of the expected class, which isn't really explained in this proposal either, then it will just be attempted to wrapped in the class object based on the expectation that it's a dictionary type. I think this may very well cause crashes. It appears in your PR that you modify that method to avoid this case: - (nullable id)sdl_objectForName:(SDLName)name ofClass:(Class)classType {
NSObject *obj = [self sdl_objectForName:name];
if ([obj isKindOfClass:classType]) {
return obj;
} else if ([obj isKindOfClass:NSDictionary.class] && [classType instancesRespondToSelector:@selector(initWithDictionary:)]) {
return [[classType alloc] initWithDictionary:(NSDictionary *)obj];
} else if ([obj isKindOfClass:NSArray.class]) {
NSArray *array = (NSArray *)obj;
if ([array.firstObject isKindOfClass:classType]) {
return array.firstObject;
}
}
return nil;
} I also think the nullability issue (returning |
@joeljfischer thank you your review.
As we see, client class gets About first part of your review, code was modified because it contains mistakes that lead to crash. Agree that it's not ideal solution, it's compromise between huge changes and protection. About nullability, I think we together understand that nil would be in situation when category returns wrong type. In this case, I as developer prefer get nil instead of |
I think we all agree to that we need to do something against illegal data types sent by head units. The question is rather how to solve the problem. Sort out illegal data and return nil solves most of the problems and works well for nullable properties it might cause other problems (and maybe lead to other kind of crashes). As @joeljfischer mentioned, the most important piece of code from this proposal is: - (nullable id)sdl_objectForName:(SDLName)name ofClass:(Class)classType {
NSObject *obj = [self sdl_objectForName:name];
if ([obj isKindOfClass:classType]) {
return obj;
} else if ([obj isKindOfClass:NSDictionary.class] && [classType instancesRespondToSelector:@selector(initWithDictionary:)]) {
return [[classType alloc] initWithDictionary:(NSDictionary *)obj];
} else if ([obj isKindOfClass:NSArray.class]) {
NSArray *array = (NSArray *)obj;
if ([array.firstObject isKindOfClass:classType]) {
return array.firstObject;
}
}
return nil;
} It comes with a couple of issues:
So here's a suggestion for non-array parameters: - (nullable id)sdl_objectForName:(SDLName)name ofClass:(Class)classType {
NSObject *obj = [self sdl_objectForName:name];
// translate dictionaries to objects
if ([obj isKindOfClass:NSDictionary.class] && [classType instancesRespondToSelector:@selector(initWithDictionary:)]) {
obj = [[classType alloc] initWithDictionary:(NSDictionary *)obj];
// update store so that the object isn't created multiple times
[self sdl_setObject:obj forName:name];
}
if ([obj isKindOfClass:classType]) {
return obj;
}
return nil;
} I'll look at the array behavior but thought to provide feedback for this method now. |
Okay... I had a look at the method for arrays and I believe the revised code below is kind of solving an issue where the vehicle returns an object (dict) instead of an array with objects. Additionally it adds some additional checks for types and sets the array with translated objects back to the store (so that retranslation doesn't happen) - (nullable NSArray *)sdl_objectsForName:(SDLName)name ofClass:(Class)classType {
NSObject *obj = [self sdl_objectForName:name];
NSArray *array;
if (!obj || [obj isKindOfClass:NSNull.class]) {
return nil;
} else if ([obj isKindOfClass:NSArray.class]) {
array = (NSArray *)obj;
} else {
// in case the parameter content was an actual object instead of an array of objects
array = @[obj];
}
// instant return for empty arrays.
if (array.count == 0) {
return array;
}
// translate arrays full of dictionaries to objects
if ([array.firstObject isKindOfClass:NSDictionary.class] && [classType instancesRespondToSelector:@selector(initWithDictionary:)]) {
// It's an array of dictionaries, make them into their class type
NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:array.count];
for (NSDictionary *dict in array) {
id newObject = [[classType alloc] initWithDictionary:dict];
if (newObject) {
[newArray addObject:newObject];
}
}
array = [newArray copy];
// update store so that the object isn't created multiple times
[self sdl_setObject:array forName:name];
}
if ([array.firstObject isKindOfClass:classType]) {
return array;
}
return nil;
} |
@kshala-ford as far as I see you suggest to add side effect to pure function, it's not good idea.
|
What about a separate method for
The main issue with this method is:
I also agree with @mvyrko that setting the object in the getter is probably not a great idea. Having the function be pure makes testing much easier. |
as variant we can expand signature of method and add default value if it's needed |
I also thought of doing this but dropped this idea because the app would always create fallback objects even if they are not needed. The caller method should be extended instead: - (nonnull NSString *)myMandatoryParameter {
NSString *param = [self sdl_objectForName:@"myMandatoryParameter"];
if (!param) {
return @"my mandatory value";
}
return param;
} Okay... string may not be the best example but this code flow avoids creating fallback objects if they not needed. I don't think we can agree to a specific value per type (0, empty array etc.) as they can still be illegal to the mobile API (minValue="1" for numbers, minSize="1" for arrays). Regarding OEM testing, @joeljfischer you mentioned in another proposal to use NSAssert. We might want to use the assert abilities. For sure, we should log error message in case of illegal data.
Isn't this a classic caching problem? I don't have a strong opinion to it but I thought it improves performance to apps. Not sure how compilers translate for loops like |
@kshala-ford I'm not sure what you mean by fallback values. The method I was thinking of was like: - (nonnull id)sdl_objectForName:(SDLName)name ofClass:(Class)classType {
NSObject *obj = [self sdl_objectForName:name];
if (obj == nil) {
if ([classType isMemberOfClass:[NSString class]]) { return @""; }
if ([classType isMemberOfClass:[NSNumber class]]) { return @0; }
if ([classType isKindOfClass:[SDLRPCStruct class]]) {
return [[classType alloc] init];
}
}
// translate dictionaries to objects
if ([obj isKindOfClass:NSDictionary.class] && [classType instancesRespondToSelector:@selector(initWithDictionary:)]) {
obj = [[classType alloc] initWithDictionary:(NSDictionary *)obj];
// update store so that the object isn't created multiple times
[self sdl_setObject:obj forName:name];
}
if ([obj isKindOfClass:classType]) {
return obj;
}
return nil;
} Extending the calling method would be a fairly massive task, in addition, there is no default value for mandatory parameters, so it doesn't seem to gain us anything.
I agree, but is it better or worse than
We definitely could assert and log an error, but I feel like we still don't know what we're going to return at runtime. All of our options are bad, because they can all lead to developer error or crashes in different cases. I'm not sure there is anything better than a bad answer here. |
@joeljfischer Looks like you example still can return
|
@joeljfischer @mvyrko I'm against this approach. The dictionary method should not return fallback values. As already mentioned there is not the value, that can always stand up as a default fallback. In the mobile API there are mandatory integer parameters, where 0 is out of range ( Currently we have 644 optional and 343 mandatory paramters in our mobile API. We have only two options here: 1. Implement fallback values individuallyLet's continue with the example of - (SDLSamplingRate)samplingRate {
return [store sdl_objectForName:SDLNameSamplingRate];
} should change to - (SDLSamplingRate)samplingRate {
SDLSamplingRate *rate = [store sdl_objectForName:SDLNameSamplingRate ofClass:SDLSamplingRate.class];
if (!rate) {
return SDLSamplingRate16KHZ;
}
return rate;
} This allows us to specify individual fallback values. This leads to the following problem: Let's stay with above example. How should we decide on a value? If the car cannot work with 16kHz but we tell an app it's 16kHz... The app may never be able to perform audio pass thru and the developer will not be able to understand why. Instead we have to analyze further and understand the audio passthru usage. It's used in id newObject = [[classType alloc] initWithDictionary:dict];
if (newObject) {
[newArray addObject:newObject];
} So - (nullable NSArray<SDLAudioPassThruCapabilities *> *)audioPassThruCapabilities {
return [parameters sdl_objectsForName:SDLNameAudioPassThruCapabilities ofClass:SDLAudioPassThruCapabilities.class];
} should be changed to - (nullable NSArray<SDLAudioPassThruCapabilities *> *)audioPassThruCapabilities {
NSArray *value = [parameters sdl_objectsForName:SDLNameAudioPassThruCapabilities ofClass:SDLAudioPassThruCapabilities.class];
if (value.count == 0) return nil;
return value; If none of the possible audio Passthru capabilities is valid we have to accept this fact and return nil. Why not empty array? Because the mobile API doesn't allow it ( I guess in some rare cases we could throw an Exception if there is just no fallback solution available. Let's take 2. Accept the truth. Decouple mandatory from nullability and return nil.Simple question: Do we want to handle up to 343 mandatory parameters individually? I think many of them are happy with May be it would be the best to decouple nullability from mandatory/optional flag. I'm afraid it's too late for this as it would require a breaking change and it increases code for the swift developers. |
Due the meeting time running out on 2019-02-26, this proposal will remain |
@joeljfischer What do you think? |
For optional parameters, I think we're agreed that returning Forgive me if I'm wrong, but none of the parameters we're talking about are mandatory. I think we actually have six options for mandatory parameters:
Here are my thoughts on these: (1) may be the way to go for RELEASE builds if we can't agree on (2) (2) I'm still a proponent of this solution. It prevents crashes and isn't a massive amount of work. When the value is out of range as mentioned by @kshala-ford, this is certainly not ideal and could lead to crashes, but is less likely to than (1). (3) I'm strongly against this solution. It's not scalable and I don't think it would provide much more help than (2) would. (4) is the barest minimum we should do. Head unit developers returning invalid objects for mandatory parameters is a massive issue that should never happen in the wild. (5) may be too harsh, but also necessary, as I noted for (4), a misconfigured head unit returning bad data for mandatory parameters is a massive issue. (6) can be dismissed out of hand. It's a massive increase in work and annoyance for swift developers, and a major version change. So, my suggestion is the following:
Lastly, this change would also affect the android platform. @joeygrover @BrettyWhite @bilal-alsharifi we need your input on what to do with misconfigured mandatory parameters. |
Agree
Can you please be more specific?
I'm not sure what parameters you mean? The whole point of this discussion is about mandatory parameters. Or do you mean the parameters of my example?
From your six options for mandatory parameters I don't see any reason following any of the options exclusively. They should be seen as tools to be used for certain cases. If you read my 1. Implement fallback values individually you will see that I was actually proposing (2), (3) and (5). (4) can be considered as an additional option. I believe, I didn't make it clear in the example. I'm strongly against following only (2). We must not procrastinate this issue and must not mask, that there was a problem. It makes it way more difficult to identify the root cause when apps crash. I grouped the mandatory parameters to types and found:
Using (2) could cause dramatic issues of any enum and any struct that has nonnull params. It makes it impossible to add exclusive behavior on critical parameters. My suggestion scales way better as you might think and allows to individually react whenever needed. I would agree to have - (nullable id)sdl_objectForName:(SDLName)name ofClass:(Class)classType error:(NSError **)error; In case of an error the I suggest the more individual solution. PS: added the search results for mandatory params |
I apologize for my lack of clarity, I meant the parameters that we have seen crashes on that have brought forth this discussion in the first place. If so, we don't need to do anything right now regarding mandatory parameters. Then we can fix the optional parameters and move forward. We only need to change our mandatory parameter strategy if we have bad head units in the wild providing bad values for mandatory parameters. Does that follow?
I agree.
Well, in my example, those would return the default value as well. I agree it's not a good thing.
I'm still against this. There are cases where providing any value just doesn't make sense, and implementing individual fallback values doesn't improve this (not to mention the massive amount of work). e.g. a touch event. What touch event would we provide? I suppose I'm leaning toward doing nothing and asserting in DEBUG, or throwing an exception at runtime at this point. Finally, if we are talking about altering the |
In Android currently, we don't have Nullable/NotNull annotations for the the parameters that are returned from the getter methods even if that param is mandatory. However, we have a
|
@joeljfischer @kshala-ford
from JSON like
In this case, we shouldn't create SomeResponse at all. Now, we try to decide should items be nil or not, but looks like it doesn't matter due to json is incorrect for us and object can't be created. |
The Steering Committee voted to accept this proposal with revisions. The Revisions will include changing the method signature to the one that returns an error noted in this comment, so in the case of an error the NSError object could return the original data and an error message, or in the future, a fallback object for mandatory parameters. |
@mvyrko please notify me when revisions have been made. |
Proposal has been updated to reflect revisions, and issue has been entered: iOS |
Hello SDL community,
The review of "SDL 0219 - Explicit returned type from NSDictionary(Store) category" begins now and runs through February 26, 2019. The proposal is available here:
https://github.com/smartdevicelink/sdl_evolution/blob/master/proposals/0219-ios-check-type.md
Reviews are an important part of the SDL evolution process. All reviews should be sent to the associated Github issue at:
#685
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of SDL. When writing your review, here are some questions you might want to answer in your review:
Please state explicitly whether you believe that the proposal should be accepted into SDL.
More information about the SDL evolution process is available at
https://github.com/smartdevicelink/sdl_evolution/blob/master/process.md
Thank you,
Jordyn Mackool
Program Manager - Livio
jordyn@livio.io
The text was updated successfully, but these errors were encountered: