Skip to main content

Mike Kreuzer

Is serializable really a word?

September 30, 2017

Swift 4 brought with it two things I've been waiting for since Swift's early days: multi-line strings and JSON serialisation. I've seen a few half implementations of a serialising protocol extensions recently, so for completeness here's a fuller version, with failable initialisation, reading & writing to files, and some date sanity.

import Foundation

protocol Serializable: Codable {
    init?(from source: Data?)
    init?(contentsOf fileURL: URL?) throws
    func save() -> Data?
    func save(to fileURL: URL) throws
}

extension Serializable {
    init?(from source: Data?) {
        guard let source = source else {
            return nil
        }
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        if let val = try? decoder.decode(Self.self, from: source) {
            self = val
        } else {
            return nil
        }
    }
    init?(contentsOf fileURL: URL?) throws {
        guard let fileURL = fileURL else {
            return nil
        }
        self.init(from: try Data(contentsOf: fileURL))
    }
    func save() -> Data? {
        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .iso8601
        return try? encoder.encode(self)
    }
    func save(to fileURL: URL) throws {
        guard let data = self.save() else {
            return
        }
        try data.write(to: fileURL, options: .atomic)
    }
}

(And I forgot to put decoder.dateDecodingStrategy in there when I first published this. Damn.)

I went with init and save rather than serialize and deserialize partly because init felt right, but also because serialize and deserialize inevitably make me scratch my head about which is which. I'm still in two minds about whether to swallow those error throws, or to demand an unwrapped URL… maybe, I mostly tend to use throws for disk operations so this mix feels right for now.

From then on serialisation is a simple function call anywhere you invoke the protocol:

struct SerializableStruct: Serializable {
    let name: String
}

let myStruct = SerializableStruct(name: "ok")
let data = myStruct.save()
let aStructAgain = SerializableStruct(from: data)

The only issue I have so far – and this is minor compared to the joy I feel at the thought of dozens of Swift to JSON libraries finally being killed by Codable – is that collections don't automagically know about this. Protocol "inheritance" only works down the chain, so for example an array of structs which are declared to be serializable has to be encoded and decoded "manually", or else wrapper inside another struct or class that is itself serializable, so:

// eg for
let plainArray = [myStruct]

//either
struct ArrayWrapper: Serializable {
    let wrapper: [SerializableStruct]
}
let wrappedArray = ArrayWrapper(wrapper: plainArray)
let dataYetAgain = wrappedArray.save()

// or
let dataYetAgainAgain = try? JSONEncoder().encode(plainArray)

// etc

Unless I'm doing something wrong of course!

Tags:

Twitter followers, Ruby redux

July 28, 2017

Today I'm briefly revisiting January's Elixir code, and rewriting that in Ruby.

Apart from dealing with rate limits, being just the one file & easier to read, the Ruby version runs from the command line and doesn't shower me with dependency build warnings…

It's a small update of the original, original Ruby version of this, to deal with the newer cursored lists that are returned by the twitter API, and uses the Twitter gem.

require 'twitter'

client = Twitter::REST::Client.new do |c|
  c.consumer_key        = 'yep'
  c.consumer_secret     = 'yep'
  c.access_token        = 'yep'
  c.access_token_secret = 'yep'
end

def get_array(method)
  cursored_list_of_ids = method
  begin
    cursored_list_of_ids.to_a
  rescue Twitter::Error::TooManyRequests => error
    puts "Rate limited for: #{error.rate_limit.reset_in / 60} minutes"
    sleep error.rate_limit.reset_in + 1
    retry
  end
end

guilty_ids = get_array(client.friend_ids) - get_array(client.follower_ids)

if guilty_ids.empty?
  puts 'Already square'
else
  puts 'Unfollowed:'
  results = client.unfollow guilty_ids
  results.each { |user| puts "#{user.name} (#{user.screen_name})" }
end

That's the whole thing. Just add in your Twitter credentials and you're good to go, "unfollowing back" to your heart's content.

Tags:

Fifty something days of code

June 28, 2017

Well that didn't work. I never did get around to finishing my 100 days of code.

I'm not unhappy with the progress I did make, and I intend to finish working on Zine, and also still have another new open source project in my sights for the immediate future.

The last few months were taken up by some paid work, and, well, a guy's got to eat. I did play around with what would have been an open source Swift project, but I abandoned it for reasons unrelated to Swift. I got enamoured with Mastodon - a Twitter competitor. Turns out though that while Twitter has a bunch of problems, Mastodon doesn't solve most of them, while adding quite a few of its own. It's not for me, I'm sticking with "the bird site".

Here, normal services will resume momentarily.

Tags:

Previously

Earlier posts...