KittyMac / Sextant

High performance JSONPath queries for Swift

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Iterating and Typing JSONArray

abegehr opened this issue Β· comments

Thank you for open sourcing this library πŸ™

I'm having some issues using the results of a values-query (.query(values: "…")). Take for example the "all authors"-query from the examples: https://www.swift-linux.com/sextant/ (first one). It returns an optional JSONArray. However if I try to cast it to [String] or map/compactMap it, the compiler strikes.
How do I cast JSONArray to continue working with it?
How do I iterate over JSONArray and run JSONPath-queries on its member-json-objects?

I'm trying to do something like this:

        let likes: [LikedPost]? = body.parsed { json in
            guard let elems = json?.query(elements: "$.likes_media_likes[*]") else {
                req.logger.warning("No elems.")
                return []
            }
            let likes = elems.compactMap { elem in
                guard
                    let title = elem.query(string: "$.title"),
                    let obj = elem.query(element: "$.string_list_data[0]"),
                    let href = obj.query(string: "$.href"),
                    let timestamp = obj.query(double: "$.timestamp")
                else {
                    req.logger.warning("Invalid item: \(elem)")
                    return
                }
                
                return LikedPost(profile: title, href: href, timestamp: .init(timeIntervalSince1970: timestamp))
            }
        }

However I cannot get it to work. Currently the compiler fails with "Cannot convert value of type 'LikedPost' to closure result type '()'" on the "return LikedPost".

There are basically three ways you can interface with sextant. Using query(values) will give you an [Any?] kind of like the JSONSerialization APIs; its more there for legacy and is very inefficient. Second way is to use Swift to coerce the values by using let results: [String] = query(). Last method is to use JsonElements, which is Sextant's internal structures.

        let json = #"[{"title":"Post 1","timestamp":1},{"title":"Post 2","timestamp":2}]"#

        // ===== Method 0: compatible with JSONSerialization =====
        // First method is like using JSONSerialization; returns a "JSONArray" which
        // is just a typealias for [Any?]. Least efficient method, annoying to use.
        let result0 = json.query(values: "$[*]")!
        
        // prints: Optional([Optional(["title": Optional("Post 1"), "timestamp": Optional(1)]), Optional(["name": Optional("Post 2"), "timestamp": Optional(2)])])
        print(result0)
        
        if let result0JsonData = try? JSONSerialization.data(withJSONObject: result0, options: [.sortedKeys]),
           // prints: [{"timestamp":1,"title":"Post 1"},{"title":"Post 2","timestamp":2}]
           let result0JsonString = String(data: result0JsonData, encoding: .utf8) {
            print(result0JsonString)
        }

        
        // ===== Method 1: coerce Codable structs =====
        // Middle efficiency, more convenient coding.
        struct LikedPost: Codable {
            let title: String
            let timestamp: Int
        }
        
        let result1: [LikedPost] = json.query("$[*]")!
        // prints: "Post 1"
        print(result1[0].title)
        
        
        // ===== Method 2a: JsonElement =====
        // JsonElements are the internal structures used by Sextant; they have references
        // which point to the original content so they can be used without any unnecessary
        // copying of data. Since they reference the original data, you need to ensure that
        // they get converted to a more real format for use elsewhere.
        
        var result2a: [LikedPost] = []
        json.query(forEach: "$[*]") { item in
            if let title = item[string: "title"],
               let timestamp = item[int: "timestamp"] {
                result2a.append(LikedPost(title: title, timestamp: timestamp))
            }
        }
        // prints: "Post 1"
        print(result2a[0].title)
        
        // ===== Method 2b: JsonElement =====
        // When working with JsonElements, its often advisable to use the parsed() method. This ensures
        // that the data the JsonElements reference is alive while using them. It also allows you to
        // perform multiple queries against the same json very efficiently, as you have the root
        // JsonElement directly for all queries.
        var result2b: [LikedPost] = []
        json.parsed { root in
            guard let root = root else { return }
            
            // Contrived: pulling the two values out using different queries just to prove the
            // point. The json is already parsed so queries inside here are very fast
            if let titles: [String] = root.query("$..title"),
               let timestamps: [Int] = root.query("$..timestamp"),
               titles.count == timestamps.count {
                for idx in 0..<titles.count {
                    result2b.append(LikedPost(title: titles[idx], timestamp: timestamps[idx]))
                }
            }
        }
        // prints: "Post 1"
        print(result2b[0].title)

Awesome, thank you @KittyMac - this is great! I used Method 0 until now and that was super fast already imo. I'll try the other methods 2a/b now as then seem the nicest.
I think the main stumble point for me was that I cannot use .map() between Sextant types and Swift types but should use forEach and appending instead. .map() seems to always return [()].