fauna 0.2.6 → 1.1.0

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,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)