Plug and Play Parsers

11 Apr 2016 5 min read

I've been building iOS apps since iOS v3, and most of those apps have had to connect to APIs to populate their UI. The technologies used to connect to these APIs and consume their content have changed over the years, but I keep coming back to the same pattern: parsing the response. I split my parsers out into focused classes that only have one responsibility - parsing the response.

You may be thinking:

"Well, yes.....that's what a parser does....."

But throughout those years building iOS apps, I've seen many different implementations that bolt extra functionality on, to go beyond parsing - some are concerned with executing on a background thread, others with notifying the UI when they are finished, and others still parse in the view controllers they are called from. None of those extras has anything to do with parsing; these extras just make the parsing logic harder to understand.

The example we are going to explore is based on an app I was recently working on that parsed a Post response object into an NSManagedObject subclass. This parsing happened on a background thread, and because of the way Core Data is configured, the parser has to use an NSManagedObjectContext set up for that thread - none of which the parser will care about.

Photo of a plug

Show me the Post

First things first, as this is a solution for supporting more than one parser, we need to abstract out the common functionality into a parent/base class that our PostParser will inherit from:

@interface WBMParser : NSObject

/**
 Convenience alloc/init that will return a parser instance.
 
 @param managedObjectContext - context that will be used to access and create NSManagedObject subclasses.
 
 @return WBMParser instance.
 */
+ (instancetype)parserWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext;

/**
 Context that will be used to access and create NSManagedObject subclasses.
 */
@property (nonatomic, strong, readonly) NSManagedObjectContext *localManagedObjectContext;

@end

In the above, we create an interface that will allow us to inject our NSManagedObjectContext instance into this parser using Constructor/Initialiser Injection. By injecting the NSManagedObjectContext instance, we free the parser from needing to fetch it itself and so simplify the parsing implementation.

Just to round it up, here is the .m:

@interface WBMParser ()

@property (nonatomic, strong, readwrite) NSManagedObjectContext *localManagedObjectContext;

@end

@implementation WBMParser

#pragma mark - Parser

+ (instancetype)parserWithManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
    WBMParser *parser = [[self.class alloc] init];
    parser.localManagedObjectContext = managedObjectContext;
    
    return parser;
}

@end

The convenience initialiser is especially important when we come to writing our unit tests - as those unit tests will execute the parser on the main context, but the app will execute it on a background context. So here we make it easier to test the parser, and we also ensure that the developer who is going to use the parser must think about how the context is configured.

Ok, so that's our parent/base class - let's take a look at the PostParser:

/**
 Post parser - creates or updates posts.
 */
@interface WBMPostParser : WBMParser

/**
 Parse's the post.
 
 @param postDictionary - JSON containing the Post.
 
 @return WBMPost instance that was parsed.
 */
- (WBMPost *)parsePost:(NSDictionary *)postDictionary;

It's a simple enough interface, one method that takes an NSDictionary instance containing the JSON response from the server (post NSJSONSerialization).

@implementation WBMPostParser

#pragma mark - Post

- (WBMPost *)parsePost:(NSDictionary *)postResponse
{
    WBMPost *post = nil;
    
    if (postResponse[@"id"])
    {
        NSString *postID = [NSString stringWithFormat:@"%@", postResponse[@"id"]];
        
        post = [WBMPost fetchPostWithID:postID
                   managedObjectContext:self.localManagedObjectContext];
        
        if (!post)
        {
            post = (WBMPost *)[NSEntityDescription cds_insertNewObjectForEntityForClass:[WBMPost class]
                                                                 inManagedObjectContext:self.localManagedObjectContext];
            
            post.postID = postID;
        }
        
        NSDateFormatter *dateFormatter = [NSDateFormatter wbm_dateFormatter];
        
        post.createdDate = WBMValueOrDefault([dateFormatter dateFromString:postResponse[@"created_at"]],
                                             post.createdDate);
        
        /*-------------------*/
        
        post.country = WBMValueOrDefault(postResponse[@"country"],
                                         post.country);
        
        /*-------------------*/
        
        post.content = WBMValueOrDefault(postResponse[@"content"],
                                         post.content);
        
        /*-------------------*/
        
        post.shareCount = WBMValueOrDefault(postResponse[@"share_count"],
                                            post.repostCount);
        
        post.localUserHasShared = WBMValueOrDefault(postResponse[@"is_shared"],
                                                    post.isRepost);
        
        post.viewCount = WBMValueOrDefault(postResponse[@"views_count"],
                                           post.viewCount);
        
    }
    
    return post;
}

@end

(You may be wondering where cds_insertNewObjectForEntityForClass came from; it's from apod called CoreDataServices that I use to simplify interacting with Core Data.)

In the above method, we pass in the JSON response and check if that post already exists, and if it does, we update it; if it doesn't, we create it. Once we have a Post instance, we update/assign to its properties and return that fully formed Post instance at the end. By passing in the JSON response rather than directly including the above method in the success/completion block/callback, we make this parser much easier to unit test - no need to mock out an API call, instead just build a valid NSDictionary instance with the response that you are expecting to be given and pass it in.

Apart from the Method Injection technique, there is nothing overly powerful about the above parser, but let's say the app changes and we now want to return the author (user) of each post in this response. Now, as we already have a profile screen for each user (did I not tell you that - sorry), we already have a UserParser - wouldn't it be great if we could treat these parsers as components and plug them into each other 😝.

Let's see how we would do that with our architecture:

#pragma mark - Post

- (WBMPost *)parsePost:(NSDictionary *)postResponse
{
    WBMPost *post = nil;
    
    if (postResponse[@"id"])
    {
        NSString *postID = [NSString stringWithFormat:@"%@", postResponse[@"id"]];
        
        post = [WBMPost fetchPostWithID:postID
                   managedObjectContext:self.localManagedObjectContext];
        
        if (!post)
        {
            post = (WBMPost *)[NSEntityDescription cds_insertNewObjectForEntityForClass:[WBMPost class]
                                                                 inManagedObjectContext:self.localManagedObjectContext];
            
            post.postID = postID;
        }
        
        NSDateFormatter *dateFormatter = [NSDateFormatter wbm_dateFormatter];
        
        post.createdDate = WBMValueOrDefault([dateFormatter dateFromString:postResponse[@"created_at"]],
                                             post.createdDate);
        
        /*-------------------*/
        
        post.country = WBMValueOrDefault(postResponse[@"country"],
                                         post.country);
        
        /*-------------------*/

        WBMUserParser *userParser = [WBMUserParser parserWithManagedObjectContext:self.localManagedObjectContext];
        
        NSDictionary *authorResponse = postResponse[@"user"];
        
        post.author = [userParser parseUser:authorResponse];

        /*-------------------*/
        
        post.content = WBMValueOrDefault(postResponse[@"content"],
                                         post.content);
        
        /*-------------------*/
        
        post.shareCount = WBMValueOrDefault(postResponse[@"share_count"],
                                            post.repostCount);
        
        post.localUserHasShared = WBMValueOrDefault(postResponse[@"is_shared"],
                                                    post.isRepost);
        
        post.viewCount = WBMValueOrDefault(postResponse[@"views_count"],
                                           post.viewCount);
        
    }
    
    return post;
}

@end

In the above method, we've added parsing the user for the post. Provided that you can agree with your server team to return the same user object in the response, it should be that easy (of course, you need to implement a UserParser, but it follows the same pattern as the PostParser, so I will leave it for you).

Ok, so that's it....

I can tell you are not convinced. Let's see one more example. Our app is growing like crazy, and we have decided that we want to support more than one type of Post - in fact, we want to support Text, Image and Video posts. In order to do so, we decide to create a new Media model class that will hold the content of the Post.

WBMMediaParser *mediaParser = [WBMMediaParser parserWithManagedObjectContext:self.localManagedObjectContext];

NSDictionary *mediaDictionary = postResponse[@"media"];    
    
post.media = [mediaParser parseMedia:mediaDictionary];

Those three lines of code are all that you need to add.

Using the parser

This parser is now free to be called from anywhere in your app and run on any thread; it makes no assumptions/demands on if you should use GCD or NSOperationQueue and on how you handle 4xx or 5xx responses; instead, it only cares about parsing data and building a valid model object.

After thoughts

There are other approaches to creating parsers or even not creating a parser at all (directly mapping the server response to the model classes' properties - I think it's best to avoid this unless you want to inherit the technical debt of the server team into your project), and each approach has its pros and cons. The Pros of the above approach are that you produce a parser layer that is decoupled and easily unit testable. The Cons are that you need to talk more to the server team.

What do you think? Let me know by getting in touch on Mastodon or Bluesky.