Permalink
Browse files

start of FaunaClient. some tests.

  • Loading branch information...
1 parent 3cfa148 commit 8d9329e0f197fc5eef9815b7fb5ea39e36e04695 @robey committed Apr 10, 2013
Showing with 312 additions and 24 deletions.
  1. +16 −0 src/fauna.coffee
  2. +132 −0 src/fauna/fauna_client.coffee
  3. +140 −0 test/test_event_sequence.coffee
  4. +4 −4 test/test_rest.coffee
  5. +20 −20 test/test_schema.coffee
View
@@ -1,4 +1,20 @@
+schema = require("./fauna/schema")
+exports.Class = schema.Class
+exports.Schema = schema.Schema
+
+fauna_client = require("./fauna/fauna_client")
+exports.EventSequence = fauna_client.EventSequence
+exports.FaunaClient = fauna_client.FaunaClient
+
+# useful for debugging
+util = require 'util'
+exports.dump = (x) -> util.inspect(x, false, null, true)
+
+
+
+# ----- RUBBISH -----
+
Q = require 'q'
util = require 'util'
@@ -0,0 +1,132 @@
+Q = require 'q'
+util = require 'util'
+
+rest = require("./rest")
+schema = require("./schema")
+
+
+topoSort = (graph) ->
+ ###
+ given a graph of { id: [id] } where each key refers to a list of the keys
+ dependent on it, return a sorted array of ids in dependency order.
+ ###
+ rv = []
+ # make a copy of 'graph' so we don't destroy it. it's mutable in JS.
+ work = {}
+ for k, v of graph then work[k] = v
+ recursing = {}
+ visit = (id) ->
+ if recursing[id]? then throw new Error("Dependency graph has a cycle")
+ recursing[id] = true
+ for d in work[id] then if work[d]? then visit(d)
+ delete work[id]
+ delete recursing[id]
+ rv.push id
+ while Object.keys(work).length > 0
+ visit(Object.keys(work)[0])
+ rv
+
+
+class EventSequence
+ constructor: (@schema, data) ->
+ @events = []
+ if data?
+ @before = data.resource.before
+ if not @before? then @before = 0
+ @after = data.resource.after
+ @unpack(data)
+ else
+ @before = 0
+ @after = 0
+
+ # unpack a response of { resource:{}, references:{} }
+ unpack: (data) ->
+ # first, sort the attached 'references' by dependency.
+ dependencies = {}
+ for refid, struct of data.references
+ dependencies[refid] = []
+ for k, v of struct.references
+ dependencies[refid].push.apply(dependencies[refid], if v instanceof Array then v else [v])
+ # then, inflate each reference.
+ references = {}
+ for refid in topoSort(dependencies)
+ struct = data.references[refid]
+ newrefs = {}
+ for k, v of struct.references
+ v = if v instanceof Array then v.map((x) -> references[x]) else references[v]
+ newrefs[k] = v
+ struct.references = newrefs
+ references[refid] = @schema.inflate(struct, @client)
+ # finally, fill in the references in the event set.
+ @events = data.resource.events
+ for event in @events
+ refid = event.resource
+ if refid? and references[refid]? then event.resource = references[refid]
+
+ # return only the objects with a "create" event and no corresponding "delete" event.
+ # objects are ordered oldest to newest.
+ toArray: ->
+ rv = []
+ for event in @events then if event.action == "create" then rv.unshift(event.resource)
+ for event in @events then if event.action == "delete"
+ rv = (x for x in rv when x._fauna.id != event.resource._fauna.id)
+ rv
+
+exports.EventSequence = EventSequence
+
+
+class FaunaClient
+ constructor: ->
+ # three kinds of authentication: owner, publisher, client, user
+ @ownerAuthentication = { username: null, password: null }
+ @publisherKey = null
+ @clientKey = null
+ @userToken = null
+ # all js/json object transformations use the schema:
+ @schema = new Schema(@)
+
+ debug: (message) -> Rest.debug(message)
+
+ protocol: "https"
+ hostname: "rest.fauna.org"
+ apiVersion: "v1"
+
+ addPrototypes: (protos...) ->
+ ###
+ Register JS prototypes as "models" to be transformed to/from json for the
+ fauna service. May be called multiple times.
+ ###
+ @schema.addPrototypes(protos...)
+
+ urlFor: (path) ->
+ "#{@protocol}://#{encodeURIComponent(@username)}:#{escape(@password)}@#{@hostname}/#{@apiVersion}/#{path}"
+
+ op: (method, path, data) ->
+ options.url = @urlFor(path)
+ if data? then options.body = JSON.stringify(data)
+ Rest.op(method, options).then (body) ->
+ if body? then JSON.parse(body) else null
+
+
+
+
+
+
+ publisherKeys:
+ get: -> @requireOwner => @asEventArray => @op("get", "keys/publisher")
+
+
+ # ----- helper decorators
+
+ requireOwner: (f) ->
+ if not @ownerAuthentication? then return Q.reject(new Error("Requires authentication as owner"))
+ f()
+
+ asEventArray: (f) ->
+ f().then (data) =>
+ (new EventSequence(@schema, data)).toArray()
+
+
+
+
+exports.FaunaClient = FaunaClient
@@ -0,0 +1,140 @@
+should = require 'should'
+Q = require 'q'
+util = require 'util'
+
+fauna = require("../lib/fauna")
+
+
+describe "EventSequence", ->
+ it "can unpack", ->
+ es = new fauna.EventSequence(new fauna.Schema(), JSON.parse(data1))
+ es.before.should.eql(0)
+ es.after.should.eql(9223372036854775)
+ es.events.length.should.eql(3)
+ # ordering is preserved:
+ es.events.map((x) -> x.ts).should.eql [ 1365020937405000, 1365020937292002, 1365020936500000 ]
+ # should be inflated:
+ es.events.map((x) -> x.resource._fauna.id).should.eql [
+ "keys/publisher/30159234559639553"
+ "keys/publisher/30159234443248641"
+ "keys/publisher/30159233572929537"
+ ]
+ es.events.map((x) -> x.resource.key).should.eql [
+ "AQAAayWp_qAAAQBrJamcsAABzAFAUI2ckXGpAt2VjWsyiA"
+ "AQAAayWp97AEAQBrJamcsAABlnqXMsjdfw3kJU44o1dpDg"
+ "AQAAayWpw9AAAQBrJamcsAABfoxWbkY-6Vd8d_er_VP_NA"
+ ]
+
+ describe "can unpack dependencies", ->
+ schema = new fauna.Schema()
+ class Cat extends fauna.Class
+ @reference "friend"
+ @field "name"
+ schema.addPrototypes Cat
+
+ it "and inflate references", ->
+ es = new fauna.EventSequence(schema, data2)
+ es.events.length.should.eql(2)
+ es.events[0].resource.name.should.eql("Spooky")
+ es.events[0].resource.friend.name.should.eql("Commie")
+ es.events[1].resource.name.should.eql("Simba")
+ es.events[1].resource.friend.name.should.eql("Spooky")
+ es.events[1].resource.friend.friend.name.should.eql("Commie")
+
+ it "and preserve object identity", ->
+ es = new fauna.EventSequence(schema, data2)
+ es.events.length.should.eql(2)
+ es.events[0].resource.friend.age = 16
+ es.events[1].resource.friend.friend.age.should.eql(16)
+ es.events[0].resource.state = "Tennessee"
+ es.events[1].resource.friend.state.should.eql("Tennessee")
+
+ it "can flatten to an array", ->
+ schema = new fauna.Schema()
+ class Cat extends fauna.Class
+ @reference "friend"
+ @field "name"
+ schema.addPrototypes Cat
+ list = (new fauna.EventSequence(schema, data2)).toArray()
+ list[0].name.should.eql("Simba")
+ list[1].name.should.eql("Spooky")
+
+
+
+# test data from the fauna documentation for GET keys/publisher
+data1 = """
+{
+ "resource" : {
+ "ref" : "keys/publisher",
+ "class" : "sets",
+ "after" : 9223372036854775,
+ "creates" : 3,
+ "updates" : 0,
+ "deletes" : 0,
+ "events" : [
+ {
+ "ts" : 1365020937405000,
+ "action" : "create",
+ "resource" : "keys/publisher/30159234559639553",
+ "set" : "keys/publisher"
+ },
+ {
+ "ts" : 1365020937292002,
+ "action" : "create",
+ "resource" : "keys/publisher/30159234443248641",
+ "set" : "keys/publisher"
+ },
+ {
+ "ts" : 1365020936500000,
+ "action" : "create",
+ "resource" : "keys/publisher/30159233572929537",
+ "set" : "keys/publisher"
+ }
+ ]
+ },
+ "references" : {
+ "keys/publisher/30159234559639553" : {
+ "ref" : "keys/publisher/30159234559639553",
+ "class" : "keys/publisher",
+ "ts" : 1365020937405000,
+ "key" : "AQAAayWp_qAAAQBrJamcsAABzAFAUI2ckXGpAt2VjWsyiA",
+ "deleted" : false
+ },
+ "keys/publisher/30159234443248641" : {
+ "ref" : "keys/publisher/30159234443248641",
+ "class" : "keys/publisher",
+ "ts" : 1365020937292002,
+ "key" : "AQAAayWp97AEAQBrJamcsAABlnqXMsjdfw3kJU44o1dpDg",
+ "deleted" : false
+ },
+ "keys/publisher/30159233572929537" : {
+ "ref" : "keys/publisher/30159233572929537",
+ "class" : "keys/publisher",
+ "ts" : 1365020936500000,
+ "key" : "AQAAayWpw9AAAQBrJamcsAABfoxWbkY-6Vd8d_er_VP_NA",
+ "deleted" : false
+ }
+ }
+}
+"""
+
+data2 =
+ resource:
+ events: [
+ { ts: 4, action: "create", resource: "spooky", set: "_" }
+ { ts: 3, action: "create", resource: "simba", set: "_" }
+ ]
+ references:
+ spooky:
+ class: "cats"
+ data: { name: "Spooky" }
+ references: { friend: "commie" }
+ commie:
+ class: "cats"
+ data: { name: "Commie" }
+ references: {}
+ simba:
+ class: "cats"
+ data: { name: "Simba" }
+ references: { friend: "spooky" }
+
View
@@ -27,16 +27,16 @@ describe "Rest", ->
error.message.should.eql("Server response: 404")
it "can PUT", futureTest ->
- r = -> Rest.put(url: "http://example.com/test1", data: { name: "Commie" })
+ r = -> Rest.put(url: "http://example.com/test1", body: "Commie")
withSuccessfulRequest("hello.", r).then ([ body, requests ]) ->
body.should.eql("hello.")
- requests.should.eql [ { url: "http://example.com/test1", body: "{\"name\":\"Commie\"}", method: "PUT" } ]
+ requests.should.eql [ { url: "http://example.com/test1", body: "Commie", method: "PUT" } ]
it "can POST", futureTest ->
- r = -> Rest.post(url: "http://example.com/test1", data: { name: "Commie" })
+ r = -> Rest.post(url: "http://example.com/test1", body: "Commie")
withSuccessfulRequest("hello.", r).then ([ body, requests ]) ->
body.should.eql("hello.")
- requests.should.eql [ { url: "http://example.com/test1", body: "{\"name\":\"Commie\"}", method: "POST" } ]
+ requests.should.eql [ { url: "http://example.com/test1", body: "Commie", method: "POST" } ]
it "can DELETE", futureTest ->
r = -> Rest.delete(url: "http://example.com/test1")
Oops, something went wrong.

0 comments on commit 8d9329e

Please sign in to comment.