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