fogli 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +14 -0
- data/LICENSE +21 -0
- data/README.md +168 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/examples/README.md +6 -0
- data/examples/me/README.md +18 -0
- data/examples/me/me.rb +30 -0
- data/lib/fogli.rb +46 -0
- data/lib/fogli/album.rb +8 -0
- data/lib/fogli/categorized_object.rb +9 -0
- data/lib/fogli/comment.rb +5 -0
- data/lib/fogli/event.rb +9 -0
- data/lib/fogli/exception.rb +15 -0
- data/lib/fogli/facebook_graph.rb +125 -0
- data/lib/fogli/facebook_object.rb +225 -0
- data/lib/fogli/facebook_object/connection_proxy.rb +80 -0
- data/lib/fogli/facebook_object/connection_scope.rb +123 -0
- data/lib/fogli/facebook_object/connections.rb +56 -0
- data/lib/fogli/facebook_object/properties.rb +81 -0
- data/lib/fogli/facebook_object/scope_methods.rb +49 -0
- data/lib/fogli/group.rb +8 -0
- data/lib/fogli/link.rb +8 -0
- data/lib/fogli/named_object.rb +8 -0
- data/lib/fogli/note.rb +7 -0
- data/lib/fogli/oauth.rb +167 -0
- data/lib/fogli/page.rb +15 -0
- data/lib/fogli/photo.rb +8 -0
- data/lib/fogli/post.rb +18 -0
- data/lib/fogli/status.rb +7 -0
- data/lib/fogli/user.rb +36 -0
- data/lib/fogli/util/module_attributes.rb +61 -0
- data/lib/fogli/util/options.rb +32 -0
- data/lib/fogli/video.rb +7 -0
- data/test/fogli/facebook_graph_test.rb +124 -0
- data/test/fogli/facebook_object/connection_proxy_test.rb +42 -0
- data/test/fogli/facebook_object/connection_scope_test.rb +91 -0
- data/test/fogli/facebook_object/connections_test.rb +50 -0
- data/test/fogli/facebook_object/properties_test.rb +79 -0
- data/test/fogli/facebook_object/scope_methods_test.rb +69 -0
- data/test/fogli/facebook_object_test.rb +178 -0
- data/test/fogli/oauth_test.rb +56 -0
- data/test/fogli/post_test.rb +18 -0
- data/test/fogli/user_test.rb +25 -0
- data/test/fogli/util/options_test.rb +38 -0
- data/test/fogli_test.rb +16 -0
- data/test/test_helper.rb +14 -0
- metadata +150 -0
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'fogli/facebook_object/properties'
|
2
|
+
require 'fogli/facebook_object/connections'
|
3
|
+
|
4
|
+
module Fogli
|
5
|
+
# Represents any facebook object. This exposes the common
|
6
|
+
# abstractions used by every other facebook object such as the
|
7
|
+
# concept of properties and connections.
|
8
|
+
#
|
9
|
+
# # Finding an Object
|
10
|
+
#
|
11
|
+
# To find any Facebook object, use the {find} method. The find
|
12
|
+
# method is _lazy_, meaning that it doesn't actually make the HTTP
|
13
|
+
# request to Facebook then, but instead defers the call to when the
|
14
|
+
# first property is accessed.
|
15
|
+
#
|
16
|
+
# user = Fogli::User["mitchellh"]
|
17
|
+
# user.first_name # HTTP request is made here
|
18
|
+
# user.last_name # Loaded already so not request is made
|
19
|
+
#
|
20
|
+
# # Finding an Object, but Selecting Specific Fields
|
21
|
+
#
|
22
|
+
# To conserve bandwidth and lower transfer time, you can select only
|
23
|
+
# specific fields of an object. An example is shown below:
|
24
|
+
#
|
25
|
+
# user = Fogli::User.find("mitchellh", :fields => [:first_name, :last_name])
|
26
|
+
# user.first_name # "Mitchell"
|
27
|
+
# user.link # nil, since we didn't request it
|
28
|
+
#
|
29
|
+
# # Checking if an Object Exists
|
30
|
+
#
|
31
|
+
# Since objects are lazy loaded, you can't check the return value of
|
32
|
+
# {find} to see if an object exists. Instead, you must use the
|
33
|
+
# dedicated {exist?} or {#exist?} methods. The difference is
|
34
|
+
# outlined below:
|
35
|
+
#
|
36
|
+
# * {#exist?} loads all of the data associated with an instance to
|
37
|
+
# check for existence, so that the data is ready to go on a property
|
38
|
+
# access.
|
39
|
+
# * {exist?} only loads the ID of an object, which uses less
|
40
|
+
# bandwidth and has less overhead associated with it. This is ideal
|
41
|
+
# for only checking if an object exists, but not caring about the
|
42
|
+
# properties.
|
43
|
+
#
|
44
|
+
class FacebookObject < FacebookGraph
|
45
|
+
autoload :ConnectionProxy, 'fogli/facebook_object/connection_proxy'
|
46
|
+
autoload :ConnectionScope, 'fogli/facebook_object/connection_scope'
|
47
|
+
autoload :ScopeMethods, 'fogli/facebook_object/scope_methods'
|
48
|
+
|
49
|
+
include Properties
|
50
|
+
include Connections
|
51
|
+
extend Util::Options
|
52
|
+
|
53
|
+
attr_reader :_fields
|
54
|
+
attr_reader :_raw
|
55
|
+
|
56
|
+
# Every facebook object has an id and typically an updated time
|
57
|
+
# (if authorized)
|
58
|
+
property :id, :updated_time
|
59
|
+
|
60
|
+
class << self
|
61
|
+
# Finds the facebook object associated with the given `id`. The
|
62
|
+
# object is **not loaded** until the first property access. To
|
63
|
+
# check if an object exists, you can call {#exist?} on the
|
64
|
+
# object itself, which will proceed to load all the relevant
|
65
|
+
# properties as well. Or, you can use the class-level {exist?}
|
66
|
+
# if you only care about if the object exists, but not about
|
67
|
+
# it's properties.
|
68
|
+
#
|
69
|
+
# For examples on how to use this method, please view the
|
70
|
+
# documentation for the entire {FacebookObject} class.
|
71
|
+
#
|
72
|
+
# @param [String] id ID of the object
|
73
|
+
# above.
|
74
|
+
# @param [Hash] options Options such as `fields`.
|
75
|
+
# @return [FacebookObject]
|
76
|
+
def find(id, options=nil)
|
77
|
+
data = { :_loaded => false, :id => id }
|
78
|
+
options = verify_options(options, :valid_keys => [:fields])
|
79
|
+
data[:_fields] = []
|
80
|
+
data[:_fields] << [options[:fields]] if options[:fields]
|
81
|
+
data[:_fields].flatten!
|
82
|
+
|
83
|
+
# Initialize the object with the loaded flag off and with the
|
84
|
+
# ID, so that the object is lazy loaded on first use.
|
85
|
+
new(data)
|
86
|
+
end
|
87
|
+
alias :[] :find
|
88
|
+
|
89
|
+
# Checks if the given object exists within the Facebook
|
90
|
+
# Graph. This method will return `true` or `false` depending on
|
91
|
+
# if the object exists. Calling {exist?} is more efficient than
|
92
|
+
# calling {#exist?} if you only care about the existence of an
|
93
|
+
# object, since {exist?} does not load all the properties
|
94
|
+
# associated with an object, which lowers bandwidth and network
|
95
|
+
# time.
|
96
|
+
#
|
97
|
+
# @param [String] id ID of the object
|
98
|
+
# @return [Boolean]
|
99
|
+
def exist?(id)
|
100
|
+
get("/#{id}", :fields => "id")
|
101
|
+
true
|
102
|
+
rescue Fogli::Exception
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
# Propagates the properties and connections to any subclasses
|
107
|
+
# which inherit from FacebookObject. This method is called
|
108
|
+
# automatically by Ruby.
|
109
|
+
def inherited(subclass)
|
110
|
+
super
|
111
|
+
|
112
|
+
propagate_properties(subclass)
|
113
|
+
propagate_connections(subclass)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Initialize a facebook object. If given some data, it will
|
118
|
+
# attempt to populate the various properties with the given data.
|
119
|
+
#
|
120
|
+
# @param [Hash] data The data for this object.
|
121
|
+
def initialize(data=nil)
|
122
|
+
data ||= {}
|
123
|
+
|
124
|
+
# Pull out any "special" values which may be in the data hash
|
125
|
+
@_loaded = !!data.delete(:_loaded)
|
126
|
+
@_fields = data.delete(:_fields) || []
|
127
|
+
@_fields.collect! { |f| f.to_sym }
|
128
|
+
|
129
|
+
populate_properties(data) if !data.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns a boolean noting if the object's data has been loaded
|
133
|
+
# yet. {FacebookObject}s are loaded on demand when the first
|
134
|
+
# attribute is accessed.
|
135
|
+
#
|
136
|
+
# @return [Boolean]
|
137
|
+
def loaded?
|
138
|
+
!!@_loaded
|
139
|
+
end
|
140
|
+
|
141
|
+
# Loads the data from Facebook. This is typically called once on
|
142
|
+
# first access of a property.
|
143
|
+
def load!
|
144
|
+
Fogli.logger.info("Fogli Load: #{self.class}[#{id}] (object_id: #{__id__})") if Fogli.logger
|
145
|
+
|
146
|
+
params = {}
|
147
|
+
params[:fields] = _fields.join(",") if !_fields.empty?
|
148
|
+
|
149
|
+
@_raw = get(params)
|
150
|
+
populate_properties(@_raw)
|
151
|
+
@_loaded = true
|
152
|
+
self
|
153
|
+
end
|
154
|
+
|
155
|
+
# Checks if the given object exists. Since these objects are lazy
|
156
|
+
# loaded, a direct {find} call doesn't check the existence of an
|
157
|
+
# object. By calling this method, it forces the data to load and
|
158
|
+
# also returns `true` or `false` depending on if the object exists
|
159
|
+
# or not.
|
160
|
+
#
|
161
|
+
# If you only care about the existence of an object and not it's
|
162
|
+
# data, use {exist?} instead, which is more efficient.
|
163
|
+
#
|
164
|
+
# @return [Boolean]
|
165
|
+
def exist?
|
166
|
+
load!
|
167
|
+
true
|
168
|
+
rescue Fogli::Exception
|
169
|
+
false
|
170
|
+
end
|
171
|
+
|
172
|
+
# Override the read property method to call {#load!} if this
|
173
|
+
# object hasn't been loaded yet.
|
174
|
+
alias_method :read_property_original, :read_property
|
175
|
+
def read_property(name)
|
176
|
+
if name.to_sym != :id
|
177
|
+
if !loaded?
|
178
|
+
load!
|
179
|
+
elsif Fogli.logger
|
180
|
+
# Cache hit, log it if enabled
|
181
|
+
Fogli.logger.info("Fogli Cache: #{self.class}[#{id}].#{name} (object_id: #{__id__})")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
read_property_original(name)
|
186
|
+
end
|
187
|
+
|
188
|
+
# Delete an object. This always requires an access token. If you
|
189
|
+
# do not yet have an access token, you must get one via {OAuth}.
|
190
|
+
def delete
|
191
|
+
# Although Facebook supposedly supports DELETE requests, we use
|
192
|
+
# their POST method since the rest client seems to have problems
|
193
|
+
# with DELETE requests.
|
194
|
+
post(:method => :delete)
|
195
|
+
end
|
196
|
+
|
197
|
+
# Override request methods to prepend object ID. When making a
|
198
|
+
# request on an instance of a facebook object, its typically
|
199
|
+
# to access a connection. To avoid repetition, we always prepend
|
200
|
+
# the root object's ID.
|
201
|
+
[:get, :post].each do |method|
|
202
|
+
define_method(method) do |*args|
|
203
|
+
url = "/#{id}"
|
204
|
+
url += args.shift.to_s if !args[0].is_a?(Hash)
|
205
|
+
super(url, *args)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Customize the inspect method to pretty print Facebook objects
|
210
|
+
# and their associated properties and connections.
|
211
|
+
def inspect
|
212
|
+
values = []
|
213
|
+
self.class.properties.each do |name, options|
|
214
|
+
value = read_property(name)
|
215
|
+
values << "#{name.inspect}=#{value.inspect}"
|
216
|
+
end
|
217
|
+
|
218
|
+
self.class.connections.each do |name, options|
|
219
|
+
values.push("#{name.inspect}=...")
|
220
|
+
end
|
221
|
+
|
222
|
+
"#<#{self.class} #{values.sort.join(", ")}>".strip
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module Fogli
|
2
|
+
class FacebookObject < FacebookGraph
|
3
|
+
# The proxy object which sits between a facebook object and one of
|
4
|
+
# it's connections. This connection proxy only represents
|
5
|
+
# connections which are a "has many" type, which happens to be
|
6
|
+
# every connection represented by the Facebook Graph except for
|
7
|
+
# "picture" which is handled as a special case.
|
8
|
+
class ConnectionProxy
|
9
|
+
include ScopeMethods
|
10
|
+
|
11
|
+
attr_reader :parent
|
12
|
+
attr_reader :connection_name
|
13
|
+
attr_reader :connection_options
|
14
|
+
|
15
|
+
# Initializes a connection proxy. The actual data associated
|
16
|
+
# with the proxy is not actually loaded until {#load!} is called.
|
17
|
+
#
|
18
|
+
# @param [FacebookObject] parent The parent object of the
|
19
|
+
# connection
|
20
|
+
# @param [Symbol] connection_name The name of the connection,
|
21
|
+
# such as 'friends'
|
22
|
+
# @param [Hash] connection_options The options associated with
|
23
|
+
# the connection.
|
24
|
+
def initialize(parent, connection_name, connection_options)
|
25
|
+
@parent = parent
|
26
|
+
@connection_name = connection_name
|
27
|
+
@connection_options = connection_options
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a scope for all of the data which is part of this
|
31
|
+
# connection.
|
32
|
+
#
|
33
|
+
# @return [ConnectionScope]
|
34
|
+
def all
|
35
|
+
# TODO: Cache this value so that the subsequent loads are also
|
36
|
+
# cached.
|
37
|
+
ConnectionScope.new(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Loads the data represented by this proxy given the scope. The
|
41
|
+
# proxy itself doesn't cache any data. All the caching is done
|
42
|
+
# on the scope itself.
|
43
|
+
#
|
44
|
+
# **Note:** This method should never be called
|
45
|
+
# manually. Instead, using the various scope methods, and this
|
46
|
+
# method will be called automatically.
|
47
|
+
#
|
48
|
+
# @param [ConnectionScope] scope
|
49
|
+
# @return [Hash]
|
50
|
+
def load(scope)
|
51
|
+
data = parent.get("/#{connection_name}", scope.options)
|
52
|
+
parse_data(data)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Parses the resulting data from a API request for a
|
56
|
+
# connection and returns the hash associated with it. This
|
57
|
+
# method replaces all the data hashes with actual
|
58
|
+
# {FacebookObject} objects.
|
59
|
+
#
|
60
|
+
# @param [Hash] data The data returned from the API call.
|
61
|
+
# @return [Hash]
|
62
|
+
def parse_data(data)
|
63
|
+
data ||= {}
|
64
|
+
data["data"] ||= []
|
65
|
+
data["data"] = data["data"].collect do |raw_item|
|
66
|
+
connection_class.new(raw_item)
|
67
|
+
end
|
68
|
+
|
69
|
+
data
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the class associated with this connection.
|
73
|
+
#
|
74
|
+
# @return [Class]
|
75
|
+
def connection_class
|
76
|
+
Fogli.const_get(connection_options[:class])
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module Fogli
|
2
|
+
class FacebookObject < FacebookGraph
|
3
|
+
# Represents a connection scope, which is just a scoped view of
|
4
|
+
# data. Since Facebook connections often represent massive amounts
|
5
|
+
# of data (e.g. a Facebook feed for a user often has thousands of
|
6
|
+
# entries), it is unfeasible for Facebook to send all this data in
|
7
|
+
# one request, or for a typical developer to want access to _all_
|
8
|
+
# of this data. Facebook solves this problem by paginating the
|
9
|
+
# data over several API calls. {ConnectionScope} allows developers
|
10
|
+
# to scope the requests using various methods such as {#since},
|
11
|
+
# {#until}, {#limit}, and {#offset}, hiding the need for
|
12
|
+
# pagination from the developer.
|
13
|
+
#
|
14
|
+
# Scopes are defined by chaining methods together. An actual
|
15
|
+
# request to Facebook is only made when the data load is
|
16
|
+
# requested:
|
17
|
+
#
|
18
|
+
# 1. {#all}
|
19
|
+
# 2. {#each}
|
20
|
+
#
|
21
|
+
# # Scoping a Connection
|
22
|
+
#
|
23
|
+
# items = User["mitchellh"].feed.since("yesterday").limit(5).all
|
24
|
+
# items.each do |item|
|
25
|
+
# ...
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
class ConnectionScope
|
29
|
+
include ScopeMethods
|
30
|
+
include Enumerable
|
31
|
+
|
32
|
+
attr_reader :proxy
|
33
|
+
attr_reader :options
|
34
|
+
attr_reader :_data
|
35
|
+
|
36
|
+
# Initializes a connection scope. The actual data associated
|
37
|
+
# with the proxy is not actually loaded until it is accessed.
|
38
|
+
#
|
39
|
+
# @param [ConnectionProxy] proxy The {ConnectionProxy} which
|
40
|
+
# this scope belongs to.
|
41
|
+
# @param [Hash] options The options built up to this point.
|
42
|
+
def initialize(proxy, options=nil)
|
43
|
+
@proxy = proxy
|
44
|
+
@options = options || {}
|
45
|
+
@_data = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Evaluates the current scope and yields to the given block for
|
49
|
+
# each item. This method automatically handles facebook's
|
50
|
+
# pagination, so the developer need not worry about it.
|
51
|
+
def each(&block)
|
52
|
+
load! if !_data
|
53
|
+
|
54
|
+
i = 0
|
55
|
+
count = 0
|
56
|
+
while true
|
57
|
+
# Sanity check to avoid nil accesses, although this should
|
58
|
+
# never happen
|
59
|
+
break if !_data || !_data[i]
|
60
|
+
|
61
|
+
Fogli.logger.info("Fogli Cache: #{log_string(i)}") if Fogli.logger
|
62
|
+
|
63
|
+
_data[i]["data"].each do |item|
|
64
|
+
count += 1
|
65
|
+
yield item
|
66
|
+
|
67
|
+
# Facebook's "limit" is a limit per page, not a total
|
68
|
+
# limit, which is what is expected, so we have to keep
|
69
|
+
# track of that manually
|
70
|
+
return if options[:limit] && count >= options[:limit].to_i
|
71
|
+
end
|
72
|
+
|
73
|
+
i += 1
|
74
|
+
|
75
|
+
if i >= _data.length
|
76
|
+
# Load the next page, but exit the loop if we're on the
|
77
|
+
# last page.
|
78
|
+
break if !load!
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Loads the next batch of data associated with this scope and
|
84
|
+
# adds it to the data array.
|
85
|
+
def load!
|
86
|
+
result = if _data.nil?
|
87
|
+
# We haven't loaded any of the data yet so start with the
|
88
|
+
# first page.
|
89
|
+
|
90
|
+
# Default the fields to be all fields of the parent
|
91
|
+
@options[:fields] ||= proxy.connection_class.properties.keys.join(",")
|
92
|
+
|
93
|
+
@_data = [proxy.load(self)]
|
94
|
+
true
|
95
|
+
else
|
96
|
+
# We want to load the next page of the data, and append it
|
97
|
+
# to the data array.
|
98
|
+
next_page = _data.last["paging"]["next"] rescue nil
|
99
|
+
_data << proxy.parse_data(FacebookGraph.raw_get(next_page)) if next_page
|
100
|
+
!next_page.nil?
|
101
|
+
end
|
102
|
+
|
103
|
+
Fogli.logger.info("Load: #{log_string(_data.length)}") if Fogli.logger && result
|
104
|
+
result
|
105
|
+
end
|
106
|
+
|
107
|
+
# Clears the data cache associated with this scope. This will
|
108
|
+
# force a reload of the data on the next call and will remove
|
109
|
+
# all references to any data items.
|
110
|
+
def clear_cache
|
111
|
+
@_data = nil
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns the common log string for a scope. This is used
|
115
|
+
# internally for the logger output, if enabled.
|
116
|
+
#
|
117
|
+
# @return [String]
|
118
|
+
def log_string(page)
|
119
|
+
"#{proxy.parent.class}/#{proxy.connection_name} (page #{page}) (object_id: #{__id__})"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Fogli
|
2
|
+
class FacebookObject < FacebookGraph
|
3
|
+
# Represents connections on a Facebook Object. Connections are
|
4
|
+
# relationships between data.
|
5
|
+
module Connections
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
# Defines a connection on the object.
|
12
|
+
def connection(*names)
|
13
|
+
options = names.last
|
14
|
+
options = {} if !options.kind_of?(Hash)
|
15
|
+
|
16
|
+
raise ArgumentError.new("`:class` is required for a connection.") if !options[:class]
|
17
|
+
|
18
|
+
names.each do |name|
|
19
|
+
next if name.kind_of?(Hash)
|
20
|
+
name = name.to_s.downcase.to_sym
|
21
|
+
connections[name] = options
|
22
|
+
|
23
|
+
# Create a method for reading the property.
|
24
|
+
define_method(name) { read_connection(name) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the connections associated with this facebook
|
29
|
+
# object.
|
30
|
+
#
|
31
|
+
# @return [Hash]
|
32
|
+
def connections
|
33
|
+
@_connections ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Propagates connections to another class. This is used
|
37
|
+
# internally for making sure that subclasses get all the
|
38
|
+
# connections associated with the parent classes.
|
39
|
+
def propagate_connections(klass)
|
40
|
+
connections.each { |n, o| klass.connection(n, o) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def read_connection(name)
|
45
|
+
connection_values[name]
|
46
|
+
end
|
47
|
+
|
48
|
+
def connection_values
|
49
|
+
@_connection_values ||= Hash.new do |h,k|
|
50
|
+
options = self.class.connections[k]
|
51
|
+
h[k] = ConnectionProxy.new(self, k, options)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|