JSONCache 1.1.0

JSONCache 1.1.0

TestsTested
LangLanguage SwiftSwift
License MIT
ReleasedLast Release Oct 2017
SwiftSwift Version 4.0
SPMSupports SPM

Maintained by Anders Blehr.


Downloads

Total19
Week1
Month2

Installs

Apps2
Apps WeekApps This Week 1
Test Targets1
powered by Segment

GitHub

Stars2
Watchers1
Forks2
Issues0
Contributors1
Pull Requests0

Code

Files9
LOCLines of Code 372


JSONCache 1.1.0





JSONCache is a thin layer on top of Core Data that seamlessly
consumes, caches and produces JSON data.

  • Automatically creates Core Data objects from JSON data, or merges
    JSON data into objects that already exist.
  • Automatically maps 1:1 and 1:N relationships based on inferred
    knowledge of your Core Data model.
  • If necessary, automatically maps between snake_case in JSON key
    names and camelCase in Core Data attribute names.
  • Generates JSON on demand, both from NSManagedObject instances, and
    from any struct that adopts the JSONifiable protocol.
  • Operates on background threads to avoid interfering with your app's
    responsiveness.
  • Both synchronous and asynchronous methods return instances of
    Result, the go-to
    implementation for Swift of a functional pattern that puts emphasis
    on the semantics of success or failure.

Read the API documentation for the complete picture.

Content

Show, don't tell

Consuming JSON

Say your backend produces JSON like this:

{
  "bands": [
    {
      "name": "Japan",
      "formed": 1974,
      "disbanded": 1991,
      "hiatus": "1982-1989",
      "description": "Initially a glam-inspired group [...]"
    },
    ...
  ],
  "band_members": [
    {
      "id": "David Sylvian in Japan",
      "musician": "David Sylvian",
      "band": "Japan",
      "instruments": "Lead vocals, keyboards, guitar",
      "joined": 1974,
      "left": 1991
    },
    ...
  ],
  "musicians": [
    {
      "name": "David Sylvian",
      "born": 1958,
      "instruments": "Vocals, guitar, keyboards"
    },
    ...
  ],
  "albums": [
    {
      "name": "Gentlemen Take Polaroids",
      "band": "Japan",
      "released": "1980-10-24T00:00:00Z",
      "label": "Virgin"
    },
    ...
  ]
}

To cache this data in your app, you create a suitable Core Data model:

And with only a few lines of code, the JSON data is safely persisted
in Core Data on your device, relationships and all:

import JSONCache

...

let jsonObject = try! JSONSerialization.jsonObject(with: jsonData) as! [String: Any]
let bands = jsonObject["bands"] as! [[String: Any]]
let bandMembers = jsonObject["band_members"] as! [[String: Any]]
let musicians = jsonObject["musicians"] as! [[String: Any]]
let albums = jsonObject["albums"] as! [[String: Any]]
        
JSONCache.casing = .snake_case
JSONCache.dateFormat = .iso8601WithSeparators
        
JSONCache.bootstrap(withModelName: "Bands") { result in
  switch result {
  case .success:
    JSONCache.stageChanges(withDictionaries: bands, forEntityWithName: "Band")
    JSONCache.stageChanges(withDictionaries: bandMembers, forEntityWithName: "BandMember")
    JSONCache.stageChanges(withDictionaries: musicians, forEntityWithName: "Musician")
    JSONCache.stageChanges(withDictionaries: albums, forEntityWithName: "Album")
    JSONCache.applyChanges { result in
      switch result {
      case .success:
        print("Data all nicely tucked in")
      case .failure(let error):
        print("An error occurred: \(error)")
      }
    }
  case .failure(let error):
    print("An error occurred: \(error)")
  }
}

If you receive additional data at a later stage it's even simpler:

let albums = jsonObject["albums"] as! [[String: Any]]

JSONCache.stageChanges(withDictionaries: albums, forEntityWithName: "Album")
JSONCache.applyChanges { result in
  switch result {
  case .success:
    print("Data all nicely tucked in")
  case .failure(let error):
    print("An error occurred: \(error)")
  }
}

Producing JSON

If your app allows producing as well as consuming data, you can
generate JSON directly from NSManagedObject instances:

switch JSONCache.fetchObject(ofType: "Band", withId: "Japan") {
  case .success(let japan):
    var japan = japan as! Band
    japan.otherNames = "Rain Tree Crow"
    
    ServerProxy.update(band: japan.toJSONDictionary()) { result in
      switch result {
      case .success:
        switch JSONCache.save() { result in
          case .success:
            print("Japan as Rain Tree Crow all nicely tucked in")
          case .failure(let error):
            print("An error occurred: \(error)")
        }
      case .failure(let error):
        print("An error occurred: \(error)")
      }
    }
  case .failure(let error):
    print("An error occurred: \(error)")
}

To create and persist new objects to the backend, you can either
create the NSManagedObject instance first and then use it to
generate JSON for the backend, or if you prefer to wait until the
record is safely persisted on the backend, you can generate JSON from
any struct that adopts the JSONifiable protocol:

struct BandInfo: JSONifiable {
  var name: String
  var bandDescription: String
  var formed: Int
  var disbanded: Int?
  var hiatus: Int?
  var otherNames: String?
}

let u2Info = BandInfo(name: "U2", bandDescription: "Dublin boys", formed: 1976, disbanded: nil, hiatus: nil, otherNames: "Passengers")

ServerProxy.save(band: u2Info.toJSONDictionary()) { result in
  switch result {
  case .success:
    u2 = NSEntityDescription.insertNewObject(forEntityName: "Band" into: JSONCache.mainContext)!
    u2.setAttributes(fromDictionary: u2Info)
    
    switch JSONCache.save() { result in
    case .success:
      print("U2 all nicely tucked in")
    case .failure(let error)
      print("An error occurred: \(error)")
    }
  case .failure(let error):
    print("An error occurred: \(error)")
}

But do tell

Key conversion

When consuming JSON, JSON keys are converted to Core Data entity
attribute names. Conversely, when producing JSON, Core Data entity
attribute names (or struct properties) are converted to JSON keys.

Key conversion consists of two steps:

  1. Convert between snake_case and camelCase as needed.
  2. Qualify or dequalify reserved words as needed.

Case conversion

The JSONCache.casing configuration parameter tells JSONCache whether
the JSON data uses .snake_case or .camelCase in key names. Case
conversion is only done if the JSON casing is .snake_case:

  • attribute_name becomes attributeName when consuming JSON.
  • attributeName becomes attribute_name when producing JSON.

Qualifying reserved words

Some words, such as description, collide with reserved Cocoa names
and may not be used as attribute names in Core Data entities. When
reserved words are received as keys in JSON data, they are
qualified. Specifically, the name of the Core Data entity that will
hold the data is prefixed onto the reserved word. If for instance the
entity name is EntityName:

  • description becomes entityNameDescription when consuming JSON.
  • entityNameDescription becomes description when producing JSON.

JSONCache only supports qualifying (prefixing) reserved words with the
name of the corresponding entity. Thus, care must be taken when naming
Core Data entity attributes whose JSON counterparts represent reserved
words.

Currently, JSONCache only supports qualifying the reserved word
description. More words may be added in the future.

Date conversion

JSONCache supports the following JSON date formats:

  • ISO 8601 with separators: 2000-08-22T12:28:00Z
  • ISO 8601 without separators: 20000822T122800Z
  • Seconds since 00:00 on 1 Jan 1970 as a double precision value:
    966947280.0

Use the JSONCache.dateFormat configuration parameter to tell
JSONCache which format to expect and/or produce.

Relationship mapping

How to ...

In order for JSONCache to automatically map a 1:1 or a 1:N
relationship, you essentially only need to tell it one thing: The
primary key of the object on the '1: end' of the relationship. You do
this in either of two ways:

  1. Use the name id for the primary key. (See Figure 1.)
  2. Create a User Info key named JC.isIdentifier for the primary key
    attribute and assign it the value true or YES (or TRUE or
    yes; both are case insensitive). (See Figure 2.)

Figure 1: Marking an entity's primary key by naming it id.

Figure 2: Marking an entity's primary key by creating a User Info key
named JC.isIdentifier and setting it to true.

The primary key must be unique within an entity, but not across
entities.

But how ...?

When JSONCache instantiates or updates an NSManagedObject instance
from a JSON dictionary, it does so by inspecting the
NSAttributeDescriptions of the NSEntityDescription that describes
the underlying entity, and assigns each attribute the corresponding
value from the JSON dictionary.

In a second pass, JSONCache inspects the NSRelationshipDescriptions
of the entity, and for each relationship that is not toMany, it
looks up the NSEntityDescription of the destination object. Through
a JSONCache extension method on NSEntityDescription, it obtains the
primary key of the destination entity. It already knows the primary
key value of the destination object from the JSON dictionary, so now
it has all the information it needs in order to look it up, either in
the set of new objects created from the JSON data, or in Core Data if
it already has been persisted. Once it has a reference to the
destination object, it hooks up the relationship and moves on to the
next item.

Consider the following JSON records:

Band

{
  "name": "Japan",
  "formed": 1974,
  "disbanded": 1991,
  "hiatus": "1982-1989",
  "description": "Initially a glam-inspired group [...]",
  "other_names": "Rain Tree Crow"
}

Album

{
  "name": "Tin Drum",
  "band": "Japan",
  "released": "1981-11-13T00:00:00Z",
  "label": "Virgin"
}

JSONCache first creates two NSManagedObject instances, one for the
band Japan, and one for the Tin Drum album, each holding all the
attributes from the corresponding JSON record. Then, in the second
pass, JSONCache looks at relationships. The Band entity participates
in 2 relationships, albums and members (see the ER diagram above),
both of which are toMany, so it does nothing. The Album entity
participates in 1 relationship, band, which is not
toMany. Through the NSRelationshipDescription describing the
band relationship, JSONCache finds that the destination entity is of
type Band, and through the NSEntityDescription describing the
destination entity, it finds that it has the primary key name. Now
JSONCache has all the information it needs. It looks up a Band
object whose name value is the same as the band value in the
Album dictionary for the Tin Drum album (i.e., 'Japan'), and
finally hooks up the relationship.

Installation

You can install JSONCache using either
CocoaPods or
Carthage.

Dependencies

Compatibility

  • Swift 4.x
  • macOS 10.12 or later
  • iOS 10.0 or later
  • watchOS 3.0 or later
  • tvOS 10.0 or later

License

JSONCache is released under the MIT license. See the
LICENSE file for details.