fauna 0.2.6 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ module Fauna
2
+ class NoContextError < StandardError
3
+ end
4
+
5
+ class Cache
6
+ attr_reader :connection
7
+
8
+ def initialize(connection)
9
+ raise ArgumentError, "Connection cannot be nil" unless connection
10
+ @cache = {}
11
+ @connection = connection
12
+ end
13
+
14
+ def get(ref, query = {}, pagination = {})
15
+ res = @cache[ref]
16
+ res = @cache[res] if res.is_a? String # non-canonical refs point to their canonical refs.
17
+
18
+ if res.nil?
19
+ response = @connection.get(ref, query.merge(pagination))
20
+ update_cache(ref, response)
21
+ res = response['resource']
22
+ end
23
+
24
+ res
25
+ end
26
+
27
+ def post(ref, data)
28
+ res = @connection.post(ref, data)
29
+ update_cache(ref, res)
30
+ res['resource']
31
+ end
32
+
33
+ def put(ref, data)
34
+ res = @connection.put(ref, data)
35
+ if res['resource']
36
+ update_cache(ref, res)
37
+ res['resource']
38
+ end
39
+ end
40
+
41
+ def delete(ref, data)
42
+ @connection.delete(ref, data)
43
+ @cache.delete(ref)
44
+ nil
45
+ end
46
+
47
+ private
48
+
49
+ def update_cache(ref, res)
50
+ # FIXME Implement set range caching
51
+ if (res['resource']['class'] != "resources" && res['resource']['class'] != "events")
52
+ @cache[ref] = res['resource']['ref'] # store the non-canonical ref as a pointer to the real one.
53
+ @cache[res['resource']['ref']] = res['resource']
54
+ end
55
+ @cache.merge!(res['references'] || {})
56
+ end
57
+ end
58
+ end
data/lib/fauna/client.rb CHANGED
@@ -1,62 +1,5 @@
1
1
  module Fauna
2
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
- # FIXME Implement set range caching
52
- if (res['resource']['class'] != "sets")
53
- @cache[ref] = res['resource'] if ref =~ %r{^users/self}
54
- @cache[res['resource']['ref']] = res['resource']
55
- @cache.merge!(res['references'] || {})
56
- end
57
- end
58
- end
59
-
60
3
  def self.context(connection)
61
4
  push_context(connection)
62
5
  yield
@@ -65,30 +8,34 @@ module Fauna
65
8
  end
66
9
 
67
10
  def self.push_context(connection)
68
- stack.push(CachingContext.new(connection))
11
+ stack.push(Fauna::Cache.new(connection))
69
12
  end
70
13
 
71
14
  def self.pop_context
72
15
  stack.pop
73
16
  end
74
17
 
75
- def self.get(ref, query = nil)
76
- this.get(ref, query)
18
+ def self.reset_context
19
+ stack = []
20
+ end
21
+
22
+ def self.get(ref, query = {}, pagination = {})
23
+ connection.get(ref, query, pagination)
77
24
  end
78
25
 
79
- def self.post(ref, data = nil)
80
- this.post(ref, data)
26
+ def self.post(ref, data = {})
27
+ connection.post(ref, data)
81
28
  end
82
29
 
83
- def self.put(ref, data = nil)
84
- this.put(ref, data)
30
+ def self.put(ref, data = {})
31
+ connection.put(ref, data)
85
32
  end
86
33
 
87
- def self.delete(ref, data = nil)
88
- this.delete(ref, data)
34
+ def self.delete(ref, data = {})
35
+ connection.delete(ref, data)
89
36
  end
90
37
 
91
- def self.this
38
+ def self.connection
92
39
  stack.last or raise NoContextError, "You must be within a Fauna::Client.context block to perform operations."
93
40
  end
94
41
 
@@ -33,9 +33,12 @@ module Fauna
33
33
  end
34
34
  end
35
35
 
36
+ attr_reader :domain, :scheme
37
+
36
38
  def initialize(params={})
37
39
  @logger = params[:logger] || nil
38
- @api_version = params[:version] || "v1/"
40
+ @domain = params[:domain] || "rest1.fauna.org"
41
+ @scheme = params[:scheme] || "https"
39
42
 
40
43
  if ENV["FAUNA_DEBUG"]
41
44
  @logger = Logger.new(STDERR)
@@ -44,38 +47,32 @@ module Fauna
44
47
 
45
48
  # Check credentials from least to most privileged, in case
46
49
  # multiple were provided
47
- @credentials = if params[:token]
48
- CGI.escape(@key = params[:token])
49
- elsif params[:client_key]
50
- CGI.escape(params[:client_key])
51
- elsif params[:publisher_key]
52
- CGI.escape(params[:publisher_key])
53
- elsif params[:email] and params[:password]
54
- "#{CGI.escape(params[:email])}:#{CGI.escape(params[:password])}"
50
+ @credentials = if params[:secret]
51
+ CGI.escape(params[:secret])
55
52
  else
56
53
  raise TypeError
57
54
  end
58
55
  rescue TypeError
59
- raise ArgumentError, "Credentials must be in the form of a hash containing either :publisher_key, :client_key, or :token, or both :email and :password."
56
+ raise ArgumentError, "Invalid secret."
60
57
  end
61
58
 
62
- def get(ref, query = nil)
59
+ def get(ref, query = {})
63
60
  parse(execute(:get, ref, nil, query))
64
61
  end
65
62
 
66
- def post(ref, data = nil)
63
+ def post(ref, data = {})
67
64
  parse(execute(:post, ref, data))
68
65
  end
69
66
 
70
- def put(ref, data = nil)
67
+ def put(ref, data = {})
71
68
  parse(execute(:put, ref, data))
72
69
  end
73
70
 
74
- def patch(ref, data = nil)
71
+ def patch(ref, data = {})
75
72
  parse(execute(:patch, ref, data))
76
73
  end
77
74
 
78
- def delete(ref, data = nil)
75
+ def delete(ref, data = {})
79
76
  execute(:delete, ref, data)
80
77
  nil
81
78
  end
@@ -83,12 +80,8 @@ module Fauna
83
80
  private
84
81
 
85
82
  def parse(response)
86
- obj = if response.empty?
87
- {}
88
- else
89
- JSON.parse(response)
90
- end
91
- obj.merge!("headers" => response.headers.stringify_keys)
83
+ obj = response.empty? ? {} : JSON.parse(response)
84
+ obj.merge! "headers" => Fauna.stringify_keys(response.headers)
92
85
  obj
93
86
  end
94
87
 
@@ -101,7 +94,7 @@ module Fauna
101
94
  end
102
95
 
103
96
  def query_string_for_logging(query)
104
- if query
97
+ if query && !query.empty?
105
98
  "?" + query.map do |k,v|
106
99
  "#{k}=#{v}"
107
100
  end.join("&")
@@ -111,17 +104,18 @@ module Fauna
111
104
  def execute(action, ref, data = nil, query = nil)
112
105
  args = { :method => action, :url => url(ref), :headers => {} }
113
106
 
114
- if query
107
+ if query && !query.empty?
115
108
  args[:headers].merge! :params => query
116
109
  end
117
110
 
118
- if data
111
+ if data && !data.empty?
119
112
  args[:headers].merge! :content_type => :json
120
113
  args.merge! :payload => data.to_json
121
114
  end
122
115
 
123
116
  if @logger
124
- log(2) { "Fauna #{action.to_s.upcase}(\"#{ref}#{query_string_for_logging(query)}\")" }
117
+ log(2) { "Fauna #{action.to_s.upcase} /#{ref}#{query_string_for_logging(query)}" }
118
+ log(4) { "Credentials: #{@credentials}" } if @debug
125
119
  log(4) { "Request JSON: #{JSON.pretty_generate(data)}" } if @debug && data
126
120
 
127
121
  t0, r0 = Process.times, Time.now
@@ -131,7 +125,7 @@ module Fauna
131
125
  real = r1.to_f - r0.to_f
132
126
  cpu = (t1.utime - t0.utime) + (t1.stime - t0.stime) + (t1.cutime - t0.cutime) + (t1.cstime - t0.cstime)
133
127
  log(4) { ["Response headers: #{JSON.pretty_generate(res.headers)}", "Response JSON: #{res}"] } if @debug
134
- 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" }
128
+ log(4) { "Response (#{res.code}): API processing #{res.headers[:x_http_request_processing_time]}ms, network latency #{((real - cpu)*1000).to_i}ms, local processing #{(cpu*1000).to_i}ms" }
135
129
 
136
130
  HANDLER.call(res)
137
131
  end
@@ -141,7 +135,7 @@ module Fauna
141
135
  end
142
136
 
143
137
  def url(ref)
144
- "https://#{@credentials}@rest.fauna.org/#{@api_version}#{ref}"
138
+ "#{@scheme}://#{@credentials}@#{@domain}/#{ref}"
145
139
  end
146
140
  end
147
141
  end
@@ -0,0 +1,39 @@
1
+ module Fauna
2
+ class NamedResource < Fauna::Resource
3
+ def name; struct['name'] end
4
+
5
+ def ref; super || "#{fauna_class}/#{name}" end
6
+
7
+ private
8
+
9
+ def post
10
+ raise Invalid, "Cannot POST to named resource."
11
+ end
12
+ end
13
+
14
+ class Database < Fauna::NamedResource
15
+ def self.new(*args); super('databases', *args) end
16
+ end
17
+
18
+ class Class < Fauna::NamedResource
19
+ def self.new(*args); super('classes', *args) end
20
+ end
21
+
22
+ class Key < Fauna::Resource
23
+ def self.new(*args); super('keys', *args) end
24
+
25
+ def database
26
+ struct['database'] || ref.split('/keys').first
27
+ end
28
+
29
+ private
30
+
31
+ def post
32
+ Fauna::Client.post("#{database}/keys", struct)
33
+ end
34
+ end
35
+
36
+ class Token < Fauna::Resource
37
+ def self.new(*args); super('tokens', *args) end
38
+ end
39
+ end
data/lib/fauna/rails.rb CHANGED
@@ -9,9 +9,10 @@ if defined?(Rails)
9
9
 
10
10
  @silent = false
11
11
 
12
- CONFIG_FILE = "#{Rails.root}/config/fauna.yml"
12
+ CONFIG_FILE = "#{Rails.root}/fauna.yml"
13
13
  LOCAL_CONFIG_FILE = "#{ENV["HOME"]}/.fauna.yml"
14
14
  APP_NAME = Rails.application.class.name.split("::").first.underscore
15
+ FIXTURES_DIR = "#{Rails.root}/test/fixtures/fauna"
15
16
 
16
17
  def self.auth!
17
18
  if File.exist? CONFIG_FILE
@@ -22,8 +23,8 @@ if defined?(Rails)
22
23
  end
23
24
 
24
25
  if !@silent
25
- if credentials["publisher_key"]
26
- STDERR.puts ">> Using Fauna publisher key #{credentials["publisher_key"].inspect} for #{APP_NAME.inspect}."
26
+ if credentials["server_key"]
27
+ STDERR.puts ">> Using Fauna server key #{credentials["server_key"].inspect} for #{APP_NAME.inspect}."
27
28
  else
28
29
  STDERR.puts ">> Using Fauna account #{credentials["email"].inspect} for #{APP_NAME.inspect}."
29
30
  end
@@ -31,18 +32,18 @@ if defined?(Rails)
31
32
  STDERR.puts ">> You can change this in config/fauna.yml or ~/.fauna.yml."
32
33
  end
33
34
 
34
- if credentials["publisher_key"]
35
- publisher_key = credentials["publisher_key"]
35
+ if credentials["server_key"]
36
+ server_key = credentials["server_key"]
36
37
  else
37
38
  self.root_connection = Connection.new(
38
39
  :email => credentials["email"],
39
40
  :password => credentials["password"],
40
41
  :logger => Rails.logger)
41
42
 
42
- publisher_key = root_connection.post("keys/publisher")["resource"]["key"]
43
+ server_key = root_connection.post("keys", "role" => "server")["resource"]["key"]
43
44
  end
44
45
 
45
- self.connection = Connection.new(publisher_key: publisher_key, logger: Rails.logger)
46
+ self.connection = Connection.new(server_key: server_key, logger: Rails.logger)
46
47
  else
47
48
  if !@silent
48
49
  STDERR.puts ">> Fauna account not configured. You can add one in config/fauna.yml."
@@ -74,7 +75,6 @@ if defined?(Rails)
74
75
  def self.install_reload_callback!
75
76
  if defined? ActionDispatch::Reloader
76
77
  ActionDispatch::Reloader.to_prepare do
77
- Fauna.configure_schema!
78
78
  Fauna.install_around_filter!
79
79
  end
80
80
  end
@@ -89,10 +89,30 @@ if defined?(Rails)
89
89
  end
90
90
  end
91
91
  end
92
+
93
+ def self.install_test_helper!
94
+ if defined? ActiveSupport::TestCase
95
+ ActiveSupport::TestCase.setup do
96
+ Fauna::Client.push_context(Fauna.connection)
97
+ end
98
+
99
+ ActiveSupport::TestCase.teardown do
100
+ Fauna::Client.pop_context
101
+ end
102
+ end
103
+ end
104
+
105
+ def self.install_console_helper!
106
+ Rails.application.class.console do
107
+ Fauna::Client.push_context(Fauna.connection)
108
+ end
109
+ end
92
110
  end
93
111
 
94
112
  Fauna.auth!
95
113
  Fauna.install_around_filter!
96
114
  Fauna.install_reload_callback!
97
115
  Fauna.install_inflections!
116
+ Fauna.install_test_helper!
117
+ Fauna.install_console_helper!
98
118
  end
@@ -1,123 +1,66 @@
1
1
  module Fauna
2
2
  class Resource
3
-
4
- def self.fields; @fields ||= [] end
5
- def self.event_sets; @event_sets ||= [] end
6
- def self.references; @references ||= [] end
7
-
8
- # config DSL
9
-
10
- class << self
11
- attr_accessor :fauna_class
12
-
13
- def fauna_class
14
- @fauna_class or raise MissingMigration, "Class #{name} has not been added to Fauna.schema."
3
+ def self.resource_subclass(fauna_class)
4
+ case fauna_class
5
+ when 'databases' then Fauna::Database
6
+ when 'classes' then Fauna::Class
7
+ when 'keys' then Fauna::Key
8
+ when 'resources' then Fauna::SetPage
9
+ when 'events' then Fauna::EventsPage
10
+ else Fauna::Resource
15
11
  end
16
-
17
- private
18
-
19
- def field(*names)
20
- names.each do |name|
21
- name = name.to_s
22
- fields << name
23
- fields.uniq!
24
-
25
- define_method(name) { data[name] }
26
- define_method("#{name}=") { |value| data[name] = value }
27
- end
28
- end
29
-
30
- def event_set(*names)
31
- args = names.last.is_a?(Hash) ? names.pop : {}
32
-
33
- names.each do |name|
34
- set_name = args[:internal] ? name.to_s : "sets/#{name}"
35
- event_sets << set_name
36
- event_sets.uniq!
37
-
38
- define_method(name.to_s) { Fauna::CustomEventSet.new("#{ref}/#{set_name}") }
39
- end
40
- end
41
-
42
- def reference(*names)
43
- names.each do |name|
44
- name = name.to_s
45
- references << name
46
- references.uniq!
47
-
48
- define_method("#{name}_ref") { references[name] }
49
- define_method("#{name}_ref=") { |ref| (ref.nil? || ref.empty?) ? references.delete(name) : references[name] = ref }
50
-
51
- define_method(name) { Fauna::Resource.find_by_ref(references[name]) if references[name] }
52
- define_method("#{name}=") { |obj| obj.nil? ? references.delete(name) : references[name] = obj.ref }
53
- end
54
- end
55
-
56
- # secondary index helper
57
-
58
- def find_by(ref, query)
59
- # TODO elimate direct manipulation of the connection
60
- response = Fauna::Client.this.connection.get(ref, query)
61
- response['resources'].map { |attributes| alloc(attributes) }
62
- rescue Fauna::Connection::NotFound
63
- []
64
- end
65
- end
66
-
67
- def self.find_by_ref(ref, query = nil)
68
- res = Fauna::Client.get(ref, query)
69
- Fauna.class_for_name(res.fauna_class).alloc(res.to_hash)
70
12
  end
71
13
 
72
- def self.create(*args)
73
- new(*args).tap { |obj| obj.save }
74
- end
75
-
76
- def self.create!(*args)
77
- new(*args).tap { |obj| obj.save! }
14
+ def self.hydrate(struct)
15
+ obj = resource_subclass(struct['class']).allocate
16
+ obj.instance_variable_set '@struct', struct
17
+ obj
78
18
  end
79
19
 
80
- def self.alloc(struct)
81
- obj = allocate
82
- obj.instance_variable_set('@struct', struct)
20
+ def self.new(fauna_class, attrs = {})
21
+ obj = resource_subclass(fauna_class).allocate
22
+ obj.instance_variable_set '@struct', { 'ref' => nil, 'ts' => nil, 'deleted' => false, 'class' => fauna_class }
23
+ obj.send(:assign, attrs)
83
24
  obj
84
25
  end
85
26
 
86
- def self.time_from_usecs(microseconds)
87
- Time.at(microseconds/1_000_000, microseconds % 1_000_000)
27
+ def self.create(*args)
28
+ new(*args).tap(&:save)
88
29
  end
89
30
 
90
- def self.usecs_from_time(time)
91
- time.to_i * 1000000 + time.usec
31
+ def self.find(ref, query = {}, pagination = {})
32
+ hydrate(Fauna::Client.get(ref, query, pagination))
92
33
  end
93
34
 
94
35
  attr_reader :struct
95
36
 
96
37
  alias :to_hash :struct
97
38
 
98
- def initialize(attrs = {})
99
- @struct = { 'ref' => nil, 'ts' => nil, 'deleted' => false }
100
- assign(attrs)
101
- end
102
-
103
39
  def ts
104
- struct['ts'] ? Resource.time_from_usecs(struct['ts']) : nil
40
+ struct['ts'] ? Fauna.time_from_usecs(struct['ts']) : nil
105
41
  end
106
42
 
107
43
  def ts=(time)
108
- struct['ts'] = Resource.usecs_from_time(time)
44
+ struct['ts'] = Fauna.usecs_from_time(time)
109
45
  end
110
46
 
111
47
  def ref; struct['ref'] end
112
48
  def fauna_class; struct['class'] end
113
49
  def deleted; struct['deleted'] end
114
- def unique_id; struct['unique_id'] end
50
+ def constraints; struct['constraints'] ||= {} end
115
51
  def data; struct['data'] ||= {} end
116
52
  def references; struct['references'] ||= {} end
117
- def changes; EventSet.new("#{ref}/changes") end
53
+
54
+ def events(pagination = {})
55
+ EventsPage.find("#{ref}/events", {}, pagination)
56
+ end
57
+
58
+ def set(name)
59
+ CustomSet.new("#{ref}/sets/#{CGI.escape(name)}")
60
+ end
118
61
 
119
62
  def eql?(other)
120
- self.class.equal?(other.class) && self.ref == other.ref && self.ref != nil
63
+ self.fauna_class == other.fauna_class && self.ref == other.ref && self.ref != nil
121
64
  end
122
65
  alias :== :eql?
123
66
 
@@ -143,34 +86,10 @@ module Fauna
143
86
 
144
87
  def deleted?; deleted end
145
88
 
146
- alias :destroyed? :deleted?
147
-
148
89
  def persisted?; !(new_record? || deleted?) end
149
90
 
150
- def errors
151
- @errors ||= ActiveModel::Errors.new(self)
152
- end
153
-
154
91
  def save
155
92
  @struct = (new_record? ? post : put).to_hash
156
- true
157
- rescue Fauna::Connection::BadRequest => e
158
- e.param_errors.each { |field, message| errors[field] = message }
159
- false
160
- end
161
-
162
- def save!
163
- save || (raise Invalid, errors.full_messages)
164
- end
165
-
166
- def update(attributes = {})
167
- assign(attributes)
168
- save
169
- end
170
-
171
- def update!(attributes = {})
172
- assign(attributes)
173
- save!
174
93
  end
175
94
 
176
95
  def delete
@@ -178,16 +97,12 @@ module Fauna
178
97
  struct['deleted'] = true
179
98
  struct.freeze
180
99
  nil
181
- rescue Fauna::Connection::NotAllowed
182
- raise Invalid, "This resource can not be destroyed."
183
100
  end
184
101
 
185
- alias :destroy :delete
186
-
187
102
  private
188
103
 
189
104
  # TODO: make this configurable, and possible to invert to a white list
190
- UNASSIGNABLE_ATTRIBUTES = %w(ref ts deleted).inject({}) { |h, attr| h.update attr => true }
105
+ UNASSIGNABLE_ATTRIBUTES = %w(ts deleted fauna_class).inject({}) { |h, attr| h.update attr => true }
191
106
 
192
107
  def assign(attributes)
193
108
  attributes.each do |name, val|
@@ -197,12 +112,10 @@ module Fauna
197
112
 
198
113
  def put
199
114
  Fauna::Client.put(ref, struct)
200
- rescue Fauna::Connection::NotAllowed
201
- raise Invalid, "This resource type can not be updated."
202
115
  end
203
116
 
204
117
  def post
205
- raise Invalid, "This resource type can not be created."
118
+ Fauna::Client.post(fauna_class, struct)
206
119
  end
207
120
 
208
121
  def getter_method(method)