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