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.
@@ -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