fauna 0.0.0 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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