fauna 0.0.0 → 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|