fauna 0.0.0 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/CHANGELOG +6 -0
- data/Gemfile +6 -0
- data/LICENSE +12 -0
- data/Manifest +31 -0
- data/README.md +285 -0
- data/Rakefile +20 -0
- data/fauna.gemspec +53 -0
- data/lib/fauna.rb +98 -0
- data/lib/fauna/client.rb +100 -0
- data/lib/fauna/connection.rb +129 -0
- data/lib/fauna/ddl.rb +145 -0
- data/lib/fauna/model.rb +61 -0
- data/lib/fauna/model/class.rb +49 -0
- data/lib/fauna/model/follow.rb +43 -0
- data/lib/fauna/model/publisher.rb +8 -0
- data/lib/fauna/model/timeline.rb +92 -0
- data/lib/fauna/model/user.rb +44 -0
- data/lib/fauna/rails.rb +81 -0
- data/lib/fauna/resource.rb +239 -0
- data/test/client_test.rb +62 -0
- data/test/connection_test.rb +37 -0
- data/test/fixtures.rb +58 -0
- data/test/model/association_test.rb +23 -0
- data/test/model/class_test.rb +61 -0
- data/test/model/follow_test.rb +47 -0
- data/test/model/publisher_test.rb +48 -0
- data/test/model/timeline_test.rb +50 -0
- data/test/model/user_test.rb +72 -0
- data/test/model/validation_test.rb +38 -0
- data/test/readme_test.rb +31 -0
- data/test/test_helper.rb +59 -0
- metadata +234 -50
- metadata.gz.sig +1 -0
data/lib/fauna/client.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
module Fauna
|
2
|
+
class Client
|
3
|
+
|
4
|
+
class NoContextError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class CachingContext
|
8
|
+
attr_reader :connection
|
9
|
+
|
10
|
+
def initialize(connection)
|
11
|
+
raise ArgumentError, "Connection cannot be nil" unless connection
|
12
|
+
@cache = {}
|
13
|
+
@connection = connection
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(ref, query = nil)
|
17
|
+
if @cache[ref]
|
18
|
+
Resource.alloc(@cache[ref])
|
19
|
+
else
|
20
|
+
res = @connection.get(ref, query)
|
21
|
+
cohere(ref, res)
|
22
|
+
Resource.alloc(res['resource'])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def post(ref, data)
|
27
|
+
res = @connection.post(ref, filter(data))
|
28
|
+
cohere(ref, res)
|
29
|
+
Resource.alloc(res['resource'])
|
30
|
+
end
|
31
|
+
|
32
|
+
def put(ref, data)
|
33
|
+
res = @connection.put(ref, filter(data))
|
34
|
+
cohere(ref, res)
|
35
|
+
Resource.alloc(res['resource'])
|
36
|
+
end
|
37
|
+
|
38
|
+
def delete(ref, data)
|
39
|
+
@connection.delete(ref, data)
|
40
|
+
@cache.delete(ref)
|
41
|
+
nil
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def filter(data)
|
47
|
+
data.select {|_, v| v }
|
48
|
+
end
|
49
|
+
|
50
|
+
def cohere(ref, res)
|
51
|
+
@cache[ref] = res['resource'] if ref =~ %r{^users/self}
|
52
|
+
@cache[res['resource']['ref']] = res['resource']
|
53
|
+
@cache.merge!(res['references'])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.context(connection)
|
58
|
+
push_context(connection)
|
59
|
+
yield
|
60
|
+
ensure
|
61
|
+
pop_context
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.push_context(connection)
|
65
|
+
stack.push(CachingContext.new(connection))
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.pop_context
|
69
|
+
stack.pop
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.get(ref, query = nil)
|
73
|
+
this.get(ref, query)
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.post(ref, data = nil)
|
77
|
+
this.post(ref, data)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.put(ref, data = nil)
|
81
|
+
this.put(ref, data)
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.delete(ref, data = nil)
|
85
|
+
this.delete(ref, data)
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.this
|
89
|
+
stack.last or raise NoContextError, "You must be within a Fauna::Client.context block to perform operations."
|
90
|
+
end
|
91
|
+
|
92
|
+
class << self
|
93
|
+
private
|
94
|
+
|
95
|
+
def stack
|
96
|
+
Thread.current[:fauna_context_stack] ||= []
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Fauna
|
2
|
+
class Connection
|
3
|
+
API_VERSION = 0
|
4
|
+
|
5
|
+
class Error < RuntimeError
|
6
|
+
attr_reader :param_errors
|
7
|
+
|
8
|
+
def initialize(message, param_errors = {})
|
9
|
+
@param_errors = param_errors
|
10
|
+
super(message)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class NotFound < Error; end
|
15
|
+
class BadRequest < Error; end
|
16
|
+
class Unauthorized < Error; end
|
17
|
+
class NotAllowed < Error; end
|
18
|
+
class NetworkError < Error; end
|
19
|
+
|
20
|
+
HANDLER = Proc.new do |res, _, _|
|
21
|
+
case res.code
|
22
|
+
when 200..299
|
23
|
+
res
|
24
|
+
when 400
|
25
|
+
json = JSON.parse(res)
|
26
|
+
raise BadRequest.new(json['error'], json['param_errors'])
|
27
|
+
when 401
|
28
|
+
raise Unauthorized, JSON.parse(res)['error']
|
29
|
+
when 404
|
30
|
+
raise NotFound, JSON.parse(res)['error']
|
31
|
+
when 405
|
32
|
+
raise NotAllowed, JSON.parse(res)['error']
|
33
|
+
else
|
34
|
+
raise NetworkError, res
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(params={})
|
39
|
+
@logger = params[:logger] || nil
|
40
|
+
|
41
|
+
if ENV["FAUNA_DEBUG"]
|
42
|
+
@logger = Logger.new(STDERR)
|
43
|
+
@debug = true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Check credentials from least to most privileged, in case
|
47
|
+
# multiple were provided
|
48
|
+
@credentials = if params[:token]
|
49
|
+
CGI.escape(@key = params[:token])
|
50
|
+
elsif params[:client_key]
|
51
|
+
CGI.escape(params[:client_key])
|
52
|
+
elsif params[:publisher_key]
|
53
|
+
CGI.escape(params[:publisher_key])
|
54
|
+
elsif params[:email] and params[:password]
|
55
|
+
"#{CGI.escape(params[:email])}:#{CGI.escape(params[:password])}"
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Credentials not defined."
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def get(ref, query = nil)
|
62
|
+
JSON.parse(execute(:get, ref, nil, query))
|
63
|
+
end
|
64
|
+
|
65
|
+
def post(ref, data = nil)
|
66
|
+
JSON.parse(execute(:post, ref, data))
|
67
|
+
end
|
68
|
+
|
69
|
+
def put(ref, data = nil)
|
70
|
+
JSON.parse(execute(:put, ref, data))
|
71
|
+
end
|
72
|
+
|
73
|
+
def patch(ref, data = nil)
|
74
|
+
JSON.parse(execute(:patch, ref, data))
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete(ref, data = nil)
|
78
|
+
execute(:delete, ref, data)
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def log(indent)
|
85
|
+
Array(yield).map do |string|
|
86
|
+
string.split("\n")
|
87
|
+
end.flatten.each do |line|
|
88
|
+
@logger.debug(" " * indent + line)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def execute(action, ref, data = nil, query = nil)
|
93
|
+
args = { :method => action, :url => url(ref), :headers => {} }
|
94
|
+
|
95
|
+
if query
|
96
|
+
args[:headers].merge! :params => query
|
97
|
+
end
|
98
|
+
|
99
|
+
if data
|
100
|
+
args[:headers].merge! :content_type => :json
|
101
|
+
args.merge! :payload => data.to_json
|
102
|
+
end
|
103
|
+
|
104
|
+
if @logger
|
105
|
+
log(2) { "Fauna #{action.to_s.upcase}(\"#{ref}\")" }
|
106
|
+
log(4) { "Request query: #{JSON.pretty_generate(query)}" } if query
|
107
|
+
log(4) { "Request JSON: #{JSON.pretty_generate(data)}" } if @debug && data
|
108
|
+
|
109
|
+
t0, r0 = Process.times, Time.now
|
110
|
+
|
111
|
+
RestClient::Request.execute(args) do |res, _, _|
|
112
|
+
t1, r1 = Process.times, Time.now
|
113
|
+
real = r1.to_f - r0.to_f
|
114
|
+
cpu = (t1.utime - t0.utime) + (t1.stime - t0.stime) + (t1.cutime - t0.cutime) + (t1.cstime - t0.cstime)
|
115
|
+
log(4) { ["Response headers: #{JSON.pretty_generate(res.headers)}", "Response JSON: #{res}"] } if @debug
|
116
|
+
log(4) { "Response (#{res.code}): API processing #{res.headers[:x_time_total]}ms, network latency #{((real - cpu)*1000).to_i}ms, local processing #{(cpu*1000).to_i}ms" }
|
117
|
+
|
118
|
+
HANDLER.call(res)
|
119
|
+
end
|
120
|
+
else
|
121
|
+
RestClient::Request.execute(args, &HANDLER)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def url(ref)
|
126
|
+
"https://#{@credentials}@rest.fauna.org/v#{API_VERSION}/#{ref}"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/fauna/ddl.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
module Fauna
|
2
|
+
class DDL
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@ddls = []
|
6
|
+
end
|
7
|
+
|
8
|
+
def configure!
|
9
|
+
@ddls.each { |ddl| ddl.configure! }
|
10
|
+
end
|
11
|
+
|
12
|
+
def load!
|
13
|
+
@ddls.each { |ddl| ddl.load! }
|
14
|
+
end
|
15
|
+
|
16
|
+
# resources
|
17
|
+
|
18
|
+
def with(__class__, args = {}, &block)
|
19
|
+
res = ResourceDDL.new(__class__, args)
|
20
|
+
res.instance_eval(&block) if block_given?
|
21
|
+
@ddls << res
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
class ResourceDDL
|
26
|
+
def initialize(__class__, args = {})
|
27
|
+
@timelines = []
|
28
|
+
@class = __class__
|
29
|
+
@class_name = args[:class_name] || fauna_class_name(@class)
|
30
|
+
@class.fauna_class_name = @class_name
|
31
|
+
|
32
|
+
unless @class <= max_super(@class_name)
|
33
|
+
raise ArgmentError "#{@class} must be a subclass of #{max_super(@class_name)}."
|
34
|
+
end
|
35
|
+
|
36
|
+
@meta = Fauna::ClassSettings.alloc('ref' => @class_name) if @class_name =~ %r{^classes/[^/]+$}
|
37
|
+
end
|
38
|
+
|
39
|
+
def configure!
|
40
|
+
Fauna.add_class(@class_name, @class) if @class
|
41
|
+
end
|
42
|
+
|
43
|
+
def load!
|
44
|
+
@meta.save! if @meta
|
45
|
+
@timelines.each { |t| t.load! }
|
46
|
+
end
|
47
|
+
|
48
|
+
def timeline(*name)
|
49
|
+
args = name.last.is_a?(Hash) ? name.pop : {}
|
50
|
+
@class.send :timeline, *name
|
51
|
+
|
52
|
+
name.each { |n| @timelines << TimelineDDL.new(@class_name, n, args) }
|
53
|
+
end
|
54
|
+
|
55
|
+
def field(*name)
|
56
|
+
@class.send :field, *name
|
57
|
+
end
|
58
|
+
|
59
|
+
def reference(*name)
|
60
|
+
@class.send :reference, *name
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def max_super(name)
|
66
|
+
case name
|
67
|
+
when "users" then Fauna::User
|
68
|
+
when "publisher" then Fauna::Publisher
|
69
|
+
when %r{^classes/[^/]+$} then Fauna::Class
|
70
|
+
else Fauna::Resource
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def fauna_class_name(__class__)
|
75
|
+
if __class__ < Fauna::User
|
76
|
+
"users"
|
77
|
+
elsif __class__ < Fauna::Publisher
|
78
|
+
"publisher"
|
79
|
+
elsif __class__ < Fauna::Class
|
80
|
+
"classes/#{__class__.name.tableize}"
|
81
|
+
else
|
82
|
+
raise ArgumentError, "Must specify a :class_name for non-default resource class #{__class__.name}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# timelines
|
88
|
+
|
89
|
+
def timeline(*name)
|
90
|
+
args = name.last.is_a?(Hash) ? name.pop : {}
|
91
|
+
name.each { |n| @ddls << TimelineDDL.new(nil, n, args) }
|
92
|
+
end
|
93
|
+
|
94
|
+
class TimelineDDL
|
95
|
+
def initialize(parent_class, name, args)
|
96
|
+
@meta = TimelineSettings.new(name, args)
|
97
|
+
end
|
98
|
+
|
99
|
+
def configure!
|
100
|
+
end
|
101
|
+
|
102
|
+
def load!
|
103
|
+
@meta.save!
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# commands
|
108
|
+
|
109
|
+
# def command(name)
|
110
|
+
# cmd = CommandDDL.new(name)
|
111
|
+
|
112
|
+
# yield cmd
|
113
|
+
# @ddls << cmd
|
114
|
+
|
115
|
+
# nil
|
116
|
+
# end
|
117
|
+
|
118
|
+
# class CommandDDL
|
119
|
+
# attr_accessor :comment
|
120
|
+
|
121
|
+
# def initialize(name)
|
122
|
+
# @actions = []
|
123
|
+
# end
|
124
|
+
|
125
|
+
# def configure!
|
126
|
+
# end
|
127
|
+
|
128
|
+
# def load!
|
129
|
+
# end
|
130
|
+
|
131
|
+
# def get(path, args = {})
|
132
|
+
# args.update method: 'GET', path: path
|
133
|
+
# args.stringify_keys!
|
134
|
+
|
135
|
+
# @actions << args
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
end
|
139
|
+
|
140
|
+
# c.command "name" do |cmd|
|
141
|
+
# cmd.comment = "foo bar"
|
142
|
+
|
143
|
+
# cmd.get "users/self", :actor => "blah", :body => {}
|
144
|
+
# end
|
145
|
+
end
|
data/lib/fauna/model.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
module Fauna
|
2
|
+
class Model < Resource
|
3
|
+
def self.inherited(base)
|
4
|
+
base.send :extend, ActiveModel::Naming
|
5
|
+
base.send :include, ActiveModel::Validations
|
6
|
+
base.send :include, ActiveModel::Conversion
|
7
|
+
|
8
|
+
# Callbacks support
|
9
|
+
base.send :extend, ActiveModel::Callbacks
|
10
|
+
base.send :include, ActiveModel::Validations::Callbacks
|
11
|
+
base.send :define_model_callbacks, :save, :create, :update, :destroy
|
12
|
+
|
13
|
+
# Serialization
|
14
|
+
base.send :include, ActiveModel::Serialization
|
15
|
+
end
|
16
|
+
|
17
|
+
# TODO: use proper class here
|
18
|
+
def self.find_by_id(id)
|
19
|
+
ref =
|
20
|
+
if self <= Fauna::User
|
21
|
+
"users/#{id}"
|
22
|
+
elsif self <= Fauna::User::Settings
|
23
|
+
"users/#{id}/settings"
|
24
|
+
else
|
25
|
+
"instances/#{id}"
|
26
|
+
end
|
27
|
+
|
28
|
+
Fauna::Resource.find(ref)
|
29
|
+
end
|
30
|
+
|
31
|
+
def id
|
32
|
+
ref.split("/").last
|
33
|
+
end
|
34
|
+
|
35
|
+
def save
|
36
|
+
if valid?
|
37
|
+
run_callbacks(:save) do
|
38
|
+
if new_record?
|
39
|
+
run_callbacks(:create) { super }
|
40
|
+
else
|
41
|
+
run_callbacks(:update) { super }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def delete
|
50
|
+
run_callbacks(:destroy) { super }
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid?
|
54
|
+
run_callbacks(:validate) { super }
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_model
|
58
|
+
self
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Fauna
|
2
|
+
class ClassSettings < Fauna::Resource; end
|
3
|
+
|
4
|
+
class Class < Fauna::Model
|
5
|
+
class << self
|
6
|
+
def inherited(base)
|
7
|
+
fc = name.split("::").last.underscore
|
8
|
+
Fauna.add_class(fc, base) unless Fauna.exists_class_for_name?(fc)
|
9
|
+
end
|
10
|
+
|
11
|
+
def ref
|
12
|
+
fauna_class_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def data
|
16
|
+
Fauna::Resource.find(fauna_class_name).data
|
17
|
+
end
|
18
|
+
|
19
|
+
def update_data!(hash = {})
|
20
|
+
meta = Fauna::Resource.find(fauna_class_name)
|
21
|
+
block_given? ? yield(meta.data) : meta.data = hash
|
22
|
+
meta.save!
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_data(hash = {})
|
26
|
+
meta = Fauna::Resource.find(fauna_class_name)
|
27
|
+
block_given? ? yield(meta.data) : meta.data = hash
|
28
|
+
meta.save
|
29
|
+
end
|
30
|
+
|
31
|
+
def __class_name__
|
32
|
+
@__class_name__ ||= begin
|
33
|
+
raise MissingMigration, "Class #{name} has not been added to Fauna.schema." if !fauna_class_name
|
34
|
+
fauna_class_name[8..-1]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_by_external_id(external_id)
|
39
|
+
find_by("instances", :external_id => external_id, :class => __class_name__).first
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def post
|
46
|
+
Fauna::Client.post("instances", struct.merge("class" => self.class.__class_name__))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|