fauna 0.0.0 → 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,43 @@
|
|
1
|
+
|
2
|
+
module Fauna
|
3
|
+
class Follow < Fauna::Model
|
4
|
+
def self.find_by_follower_and_resource(follower, resource)
|
5
|
+
find(new(:follower => follower, :resource => resource).ref)
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(attrs = {})
|
9
|
+
super({})
|
10
|
+
attrs.stringify_keys!
|
11
|
+
follower_ref = attrs['follower_ref']
|
12
|
+
follower_ref = attrs['follower'].ref if attrs['follower']
|
13
|
+
resource_ref = attrs['resource_ref']
|
14
|
+
resource_ref = attrs['resource'].ref if attrs['resource']
|
15
|
+
ref = "#{follower_ref}/follows/#{resource_ref}"
|
16
|
+
|
17
|
+
raise ArgumentError, "Follower ref is nil." if follower_ref.nil?
|
18
|
+
raise ArgumentError, "Resource ref is nil." if resource_ref.nil?
|
19
|
+
|
20
|
+
@struct = { 'ref' => ref, 'follower' => follower_ref, 'resource' => resource_ref }
|
21
|
+
end
|
22
|
+
|
23
|
+
def follower_ref
|
24
|
+
struct['follower']
|
25
|
+
end
|
26
|
+
|
27
|
+
def follower
|
28
|
+
Fauna::Resource.find(follower_ref)
|
29
|
+
end
|
30
|
+
|
31
|
+
def resource_ref
|
32
|
+
struct['resource']
|
33
|
+
end
|
34
|
+
|
35
|
+
def resource
|
36
|
+
Fauna::Resource.find(resource_ref)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(*args)
|
40
|
+
raise Fauna::Invalid, "Follows have nothing to update."
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
|
2
|
+
module Fauna
|
3
|
+
class TimelineEvent
|
4
|
+
|
5
|
+
attr_reader :ts, :timeline_ref, :resource_ref, :action
|
6
|
+
|
7
|
+
def initialize(attrs)
|
8
|
+
# TODO v1
|
9
|
+
# @ts = attrs['ts']
|
10
|
+
# @timeline_ref = attrs['timeline']
|
11
|
+
# @resource_ref = attrs['resource']
|
12
|
+
# @action = attrs['action']
|
13
|
+
@ts, @action, @resource_ref = *attrs
|
14
|
+
end
|
15
|
+
|
16
|
+
def resource
|
17
|
+
Fauna::Resource.find(resource_ref)
|
18
|
+
end
|
19
|
+
|
20
|
+
def timeline
|
21
|
+
Timeline.new(timeline_ref)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class TimelinePage < Fauna::Resource
|
26
|
+
def events
|
27
|
+
@events ||= struct['events'].map { |e| TimelineEvent.new(e) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def any?
|
31
|
+
struct['events'].any?
|
32
|
+
end
|
33
|
+
|
34
|
+
def resources
|
35
|
+
# TODO duplicates can exist in the local timeline. remove w/ v1
|
36
|
+
seen = {}
|
37
|
+
events.inject([]) do |a, ev|
|
38
|
+
if (ev.action == 'create' && !seen[ev.resource_ref])
|
39
|
+
seen[ev.resource_ref] = true
|
40
|
+
a << ev.resource
|
41
|
+
end
|
42
|
+
|
43
|
+
a
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Timeline
|
49
|
+
attr_reader :ref
|
50
|
+
|
51
|
+
def initialize(ref)
|
52
|
+
@ref = ref
|
53
|
+
end
|
54
|
+
|
55
|
+
def page(query = nil)
|
56
|
+
TimelinePage.find(ref, query)
|
57
|
+
end
|
58
|
+
|
59
|
+
def events(query = nil)
|
60
|
+
page(query).events
|
61
|
+
end
|
62
|
+
|
63
|
+
def resources(query = nil)
|
64
|
+
page(query).resources
|
65
|
+
end
|
66
|
+
|
67
|
+
def add(resource)
|
68
|
+
self.class.add(ref, resource)
|
69
|
+
end
|
70
|
+
|
71
|
+
def remove(resource)
|
72
|
+
self.class.remove(ref, resource)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.add(ref, resource)
|
76
|
+
resource = resource.ref if resource.respond_to?(:ref)
|
77
|
+
Fauna::Client.post(ref, 'resource' => resource)
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.remove(ref, resource)
|
81
|
+
resource = resource.ref if resource.respond_to?(:ref)
|
82
|
+
Fauna::Client.delete(ref, 'resource' => resource)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class TimelineSettings < Fauna::Resource
|
87
|
+
def initialize(name, attrs = {})
|
88
|
+
super(attrs)
|
89
|
+
struct['ref'] = "timelines/#{name}"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
module Fauna
|
3
|
+
class User < Fauna::Model
|
4
|
+
|
5
|
+
validates :name, :presence => true
|
6
|
+
|
7
|
+
class Settings < Fauna::Model; end
|
8
|
+
|
9
|
+
def self.self
|
10
|
+
find("users/self")
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find_by_email(email)
|
14
|
+
find_by("users", :email => email)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.find_by_external_id(external_id)
|
18
|
+
find_by("users", :external_id => external_id)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.find_by_name(name)
|
22
|
+
find_by("users", :name => name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def email; struct['email']; end
|
26
|
+
|
27
|
+
def password; struct['password']; end
|
28
|
+
|
29
|
+
# FIXME https://github.com/fauna/issues/issues/16
|
30
|
+
def name
|
31
|
+
struct['name']
|
32
|
+
end
|
33
|
+
|
34
|
+
def settings
|
35
|
+
Fauna::User::Settings.find("#{ref}/settings")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def post
|
41
|
+
Fauna::Client.post("users", struct)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/fauna/rails.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'fauna'
|
2
|
+
|
3
|
+
# Various and sundry Rails integration points
|
4
|
+
|
5
|
+
if defined?(Rails)
|
6
|
+
module Fauna
|
7
|
+
mattr_accessor :root_connection
|
8
|
+
mattr_accessor :connection
|
9
|
+
|
10
|
+
@silent = false
|
11
|
+
|
12
|
+
CONFIG_FILE = "#{Rails.root}/config/fauna.yml"
|
13
|
+
LOCAL_CONFIG_FILE = "#{ENV["HOME"]}/.fauna.yml"
|
14
|
+
APP_NAME = Rails.application.class.name.split("::").first.underscore
|
15
|
+
|
16
|
+
def self.auth!
|
17
|
+
if File.exist? CONFIG_FILE
|
18
|
+
credentials = YAML.load_file(CONFIG_FILE)[Rails.env] || {}
|
19
|
+
|
20
|
+
if File.exist? LOCAL_CONFIG_FILE
|
21
|
+
credentials.merge!((YAML.load_file(LOCAL_CONFIG_FILE)[APP_NAME] || {})[Rails.env] || {})
|
22
|
+
end
|
23
|
+
|
24
|
+
if !@silent
|
25
|
+
STDERR.puts ">> Using Fauna account #{credentials["email"].inspect} for #{APP_NAME.inspect}."
|
26
|
+
STDERR.puts ">> You can change this in config/fauna.yml or ~/.fauna.yml."
|
27
|
+
end
|
28
|
+
|
29
|
+
self.root_connection = Connection.new(
|
30
|
+
:email => credentials["email"],
|
31
|
+
:password => credentials["password"],
|
32
|
+
:logger => Rails.logger)
|
33
|
+
|
34
|
+
publisher_key = root_connection.post("keys/publisher")["resource"]["key"]
|
35
|
+
self.connection = Connection.new(publisher_key: publisher_key, logger: Rails.logger)
|
36
|
+
else
|
37
|
+
if !@silent
|
38
|
+
STDERR.puts ">> Fauna account not configured. You can add one in config/fauna.yml."
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
@silent = true
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
Fauna.auth!
|
48
|
+
|
49
|
+
# Around filter to set up a default context
|
50
|
+
|
51
|
+
if Fauna.connection && defined?(ActionController::Base)
|
52
|
+
ApplicationController
|
53
|
+
|
54
|
+
class ApplicationController
|
55
|
+
around_filter :default_fauna_context
|
56
|
+
|
57
|
+
def default_fauna_context
|
58
|
+
Fauna::Client.context(Fauna.connection) { yield }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# ActionDispatch's Auto reloader blows away some of Fauna's schema
|
64
|
+
# configuration that does not live within the Model classes
|
65
|
+
# themselves. Add a callback to Reloader to reload the schema config
|
66
|
+
# before each request.
|
67
|
+
|
68
|
+
if defined? ActionDispatch::Reloader
|
69
|
+
ActionDispatch::Reloader.to_prepare do
|
70
|
+
Fauna.configure_schema!
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# ActiveSupport::Inflector's 'humanize' method handles the _id
|
75
|
+
# suffix for association fields, but not _ref.
|
76
|
+
if defined? ActiveSupport::Inflector
|
77
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
78
|
+
inflect.human /_ref$/i, ''
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,239 @@
|
|
1
|
+
module Fauna
|
2
|
+
class Resource
|
3
|
+
|
4
|
+
def self.fields; @fields ||= [] end
|
5
|
+
def self.timelines; @timelines ||= [] end
|
6
|
+
def self.references; @references ||= [] end
|
7
|
+
|
8
|
+
# config DSL
|
9
|
+
|
10
|
+
class << self
|
11
|
+
attr_accessor :fauna_class_name
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def field(*names)
|
16
|
+
names.each do |name|
|
17
|
+
name = name.to_s
|
18
|
+
fields << name
|
19
|
+
fields.uniq!
|
20
|
+
|
21
|
+
define_method(name) { data[name] }
|
22
|
+
define_method("#{name}=") { |value| data[name] = value }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def timeline(*names)
|
27
|
+
args = names.last.is_a?(Hash) ? names.pop : {}
|
28
|
+
|
29
|
+
names.each do |name|
|
30
|
+
timeline_name = args[:internal] ? name.to_s : "timelines/#{name}"
|
31
|
+
timelines << timeline_name
|
32
|
+
timelines.uniq!
|
33
|
+
|
34
|
+
define_method(name.to_s) { Fauna::Timeline.new("#{ref}/#{timeline_name}") }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def reference(*names)
|
39
|
+
names.each do |name|
|
40
|
+
name = name.to_s
|
41
|
+
references << name
|
42
|
+
references.uniq!
|
43
|
+
|
44
|
+
define_method("#{name}_ref") { references[name] }
|
45
|
+
define_method("#{name}_ref=") { |ref| (ref.nil? || ref.empty?) ? references.delete(name) : references[name] = ref }
|
46
|
+
|
47
|
+
define_method(name) { Fauna::Resource.find(references[name]) if references[name] }
|
48
|
+
define_method("#{name}=") { |obj| obj.nil? ? references.delete(name) : references[name] = obj.ref }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# secondary index helper
|
53
|
+
|
54
|
+
def find_by(ref, query)
|
55
|
+
# TODO elimate direct manipulation of the connection
|
56
|
+
response = Fauna::Client.this.connection.get(ref, query)
|
57
|
+
response['resources'].map { |attributes| alloc(attributes) }
|
58
|
+
rescue Fauna::Connection::NotFound
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.find(ref, query = nil)
|
64
|
+
res = Fauna::Client.get(ref, query)
|
65
|
+
Fauna.class_for_name(res.fauna_class_name).alloc(res.to_hash)
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.create(*args)
|
69
|
+
new(*args).tap { |obj| obj.save }
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.create!(*args)
|
73
|
+
new(*args).tap { |obj| obj.save! }
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.alloc(struct)
|
77
|
+
obj = allocate
|
78
|
+
obj.instance_variable_set('@struct', struct)
|
79
|
+
obj
|
80
|
+
end
|
81
|
+
|
82
|
+
attr_reader :struct
|
83
|
+
|
84
|
+
alias :to_hash :struct
|
85
|
+
|
86
|
+
def initialize(attrs = {})
|
87
|
+
@struct = { 'ref' => nil, 'ts' => nil, 'deleted' => false }
|
88
|
+
assign(attrs)
|
89
|
+
end
|
90
|
+
|
91
|
+
def ref; struct['ref'] end
|
92
|
+
def ts; struct['ts'] end
|
93
|
+
def deleted; struct['deleted'] end
|
94
|
+
def external_id; struct['external_id'] end
|
95
|
+
def data; struct['data'] ||= {} end
|
96
|
+
def references; struct['references'] ||= {} end
|
97
|
+
def changes; Timeline.new("#{ref}/changes") end
|
98
|
+
def user_follows; Timeline.new("#{ref}/follows/users") end
|
99
|
+
def user_followers; Timeline.new("#{ref}/followers/users") end
|
100
|
+
def instance_follows; Timeline.new("#{ref}/follows/instances") end
|
101
|
+
def instance_followers; Timeline.new("#{ref}/followers/instances") end
|
102
|
+
def local; Timeline.new("#{ref}/local") end
|
103
|
+
|
104
|
+
def eql?(other)
|
105
|
+
self.class.equal?(other.class) && self.ref == other.ref && self.ref != nil
|
106
|
+
end
|
107
|
+
alias :== :eql?
|
108
|
+
|
109
|
+
|
110
|
+
# dynamic field access
|
111
|
+
|
112
|
+
def respond_to?(method, *args)
|
113
|
+
!!getter_method(method) || !!setter_method(method) || super
|
114
|
+
end
|
115
|
+
|
116
|
+
def method_missing(method, *args)
|
117
|
+
if field = getter_method(method)
|
118
|
+
struct[field]
|
119
|
+
elsif field = setter_method(method)
|
120
|
+
struct[field] = args.first
|
121
|
+
else
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# object lifecycle
|
127
|
+
|
128
|
+
def new_record?; ref.nil? end
|
129
|
+
|
130
|
+
def deleted?; deleted end
|
131
|
+
|
132
|
+
alias :destroyed? :deleted?
|
133
|
+
|
134
|
+
def persisted?; !(new_record? || deleted?) end
|
135
|
+
|
136
|
+
def errors
|
137
|
+
@errors ||= ActiveModel::Errors.new(self)
|
138
|
+
end
|
139
|
+
|
140
|
+
def save
|
141
|
+
@struct = (new_record? ? post : put).to_hash
|
142
|
+
true
|
143
|
+
rescue Fauna::Connection::BadRequest => e
|
144
|
+
e.param_errors.each { |field, message| errors[field] = message }
|
145
|
+
false
|
146
|
+
end
|
147
|
+
|
148
|
+
def save!
|
149
|
+
save || (raise Invalid, errors.full_messages)
|
150
|
+
end
|
151
|
+
|
152
|
+
def update(attributes = {})
|
153
|
+
assign(attributes)
|
154
|
+
save
|
155
|
+
end
|
156
|
+
|
157
|
+
def update!(attributes = {})
|
158
|
+
assign(attributes)
|
159
|
+
save!
|
160
|
+
end
|
161
|
+
|
162
|
+
def delete
|
163
|
+
Fauna::Client.delete(ref) if persisted?
|
164
|
+
struct['deleted'] = true
|
165
|
+
struct.freeze
|
166
|
+
nil
|
167
|
+
rescue Fauna::Connection::NotAllowed
|
168
|
+
raise Invalid, "This resource can not be destroyed."
|
169
|
+
end
|
170
|
+
|
171
|
+
alias :destroy :delete
|
172
|
+
|
173
|
+
# TODO eliminate/simplify once v1 drops
|
174
|
+
|
175
|
+
def fauna_class_name
|
176
|
+
@_fauna_class_name ||=
|
177
|
+
case ref
|
178
|
+
when %r{^users/[^/]+$}
|
179
|
+
"users"
|
180
|
+
when %r{^instances/[^/]+$}
|
181
|
+
"classes/#{struct['class']}"
|
182
|
+
when %r{^[^/]+/[^/]+/follows/[^/]+/[^/]+$}
|
183
|
+
"follows"
|
184
|
+
when %r{^.+/timelines/[^/]+$}
|
185
|
+
"timelines"
|
186
|
+
when %r{^.+/changes$}
|
187
|
+
"timelines"
|
188
|
+
when %r{^.+/local$}
|
189
|
+
"timelines"
|
190
|
+
when %r{^.+/follows/[^/]+$}
|
191
|
+
"timelines"
|
192
|
+
when %r{^.+/followers/[^/]+$}
|
193
|
+
"timelines"
|
194
|
+
when %r{^timelines/[^/]+$}
|
195
|
+
"timelines/settings"
|
196
|
+
when %r{^classes/[^/]+$}
|
197
|
+
"classes"
|
198
|
+
when %r{^users/[^/]+/settings$}
|
199
|
+
"users/settings"
|
200
|
+
when "publisher/settings"
|
201
|
+
"publisher/settings"
|
202
|
+
when "publisher"
|
203
|
+
"publisher"
|
204
|
+
else
|
205
|
+
nil
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
# TODO: make this configurable, and possible to invert to a white list
|
212
|
+
UNASSIGNABLE_ATTRIBUTES = %w(ref ts deleted).inject({}) { |h, attr| h.update attr => true }
|
213
|
+
|
214
|
+
def assign(attributes)
|
215
|
+
attributes.each do |name, val|
|
216
|
+
send "#{name}=", val unless UNASSIGNABLE_ATTRIBUTES[name.to_s]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def put
|
221
|
+
Fauna::Client.put(ref, struct)
|
222
|
+
rescue Fauna::Connection::NotAllowed
|
223
|
+
raise Invalid, "This resource type can not be updated."
|
224
|
+
end
|
225
|
+
|
226
|
+
def post
|
227
|
+
raise Invalid, "This resource type can not be created."
|
228
|
+
end
|
229
|
+
|
230
|
+
def getter_method(method)
|
231
|
+
field = method.to_s
|
232
|
+
struct.include?(field) ? field : nil
|
233
|
+
end
|
234
|
+
|
235
|
+
def setter_method(method)
|
236
|
+
(/(.*)=$/ =~ method.to_s) ? $1 : nil
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|