Skip to content

Instantly share code, notes, and snippets.

@nubbel
Created November 2, 2017 22:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nubbel/a67713415e96a8fcb43a2082872f5d3a to your computer and use it in GitHub Desktop.
Save nubbel/a67713415e96a8fcb43a2082872f5d3a to your computer and use it in GitHub Desktop.
Apollo-Link Context
//: Playground - noun: a place where people can play
import Foundation
public struct ContextKey<Value> {
public let name: String
public let defaultValue: () -> Value
public init(_ name: String, default defaultValue: @escaping @autoclosure () -> Value) {
self.name = name
self.defaultValue = defaultValue
}
}
extension ContextKey: Equatable {
public static func ==(lhs: ContextKey<Value>, rhs: ContextKey<Value>) -> Bool {
return lhs.name == rhs.name
}
}
extension ContextKey: Hashable {
public var hashValue: Int {
return name.hashValue
}
}
public final class Context {
private var data = [AnyHashable: Any]()
public func get<Value>(_ key: ContextKey<Value>) -> Value {
return data[key, default: key.defaultValue()] as! Value
}
public func set<Value>(_ value: Value, for key: ContextKey<Value>) {
data[key] = value
}
public subscript<Value>(_ key: ContextKey<Value>) -> Value {
get {
return get(key)
}
set {
set(newValue, for: key)
}
}
}
public enum ContextKeys {
}
// in HTTP link
extension ContextKeys {
public static let HTTPHeaders = ContextKey<[String: String]>("http.headers", default: [:])
public static let HTTPMethod = ContextKey<String>("http.method", default: "GET")
}
// Usage
let context = Context()
// in Auth link
context[ContextKeys.HTTPHeaders]["Authorization"] = "Bearer my-jwt-token"
context[ContextKeys.HTTPHeaders] // => ["Authorization": "Bearer my-jwt-token"]
// in another link
context[ContextKeys.HTTPMethod] = "POST"
var headers = context[ContextKeys.HTTPHeaders]
headers["X-Environment"] = "development"
headers["Content-Type"] = "application/json"
context[ContextKeys.HTTPHeaders] = headers
// in HTTP link
context[ContextKeys.HTTPMethod] // => POST
context[ContextKeys.HTTPHeaders] // => ["Content-Type": "application/json", "X-Environment": "development", "Authorization": "Bearer my-jwt-token"]
@martijnwalraven
Copy link

I like the experiment :) It's a shame you can't store static properties in generic types, because I'd rather allow people to use context[.HTTPMethod] than context[ContextKeys.HTTPMethod]. If found a little trick you can use if you make Key a class and let it inherit from a non-generic superclass where you put the static properties on:

public struct Context {
  private var data = [AnyHashable: Any]()
  
  public class Keys {
  }
  
  public class Key<Value>: Keys {
    public let name: String
    
    public init(_ name: String) {
      self.name = name
    }
  }
  
  public subscript<Value>(_ key: Key<Value>) -> Value {
    get {
      return data[key] as! Value
    }
    set {
      data[key] = newValue
    }
  }
}

extension Context.Key: Equatable {
  public static func ==(lhs: Context.Key<Value>, rhs: Context.Key<Value>) -> Bool {
    return lhs.name == rhs.name
  }
}

extension Context.Key: Hashable {
  public var hashValue: Int {
    return name.hashValue
  }
}

extension Context.Keys {
  public static let HTTPMethod = Context.Key<String>("http.method")
}

var context = Context()
context[.HTTPMethod] = "GET"

(I made Context a struct because I think value semantics make sense here.)

As much as I like experimenting however, it seems adding computed properties directly to Context would be a simpler solution:

public struct Context {
  private var data = [AnyHashable: Any]()
}

extension Context {
  var HTTPMethod: String {
    get {
      return data["HTTPMethod"] as! String
    }
    set {
      data["HTTPMethod"] = newValue
    }
  }
  
  var HTTPHeaders: [String: String] {
    get {
      return (data["HTTPHeaders"] as? [String: String]) ?? [:]
    }
    set {
      data["HTTPHeaders"] = newValue
    }
  }
}

var context = Context()
context.HTTPMethod = "GET"
context.HTTPHeaders["Authorization"] = "Bearer my-jwt-token"

What do you think?

@nubbel
Copy link
Author

nubbel commented Nov 5, 2017

Nifty little trick, I like it!
I'm not sure if value semantics actually do make sense here. In each link, after making changes to the context, one would need to remember to call the context setter on the operation. Example:

class SomeLink {
  func request(op Operation) {
    var ctx = op.context
    // do something with the context
    op.context = ctx // do not forget this!
  }
}

Wouldn't it be more convenient to just do the changes to the context via a reference type?

Regarding your simpler solution: I agree, it is indeed much more simpler and also provides the simplest API possible.
However, for third-party links we would need to make data public in order to allow them to add accessors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment