fogli 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +5 -0
  2. data/Gemfile +14 -0
  3. data/LICENSE +21 -0
  4. data/README.md +168 -0
  5. data/Rakefile +37 -0
  6. data/VERSION +1 -0
  7. data/examples/README.md +6 -0
  8. data/examples/me/README.md +18 -0
  9. data/examples/me/me.rb +30 -0
  10. data/lib/fogli.rb +46 -0
  11. data/lib/fogli/album.rb +8 -0
  12. data/lib/fogli/categorized_object.rb +9 -0
  13. data/lib/fogli/comment.rb +5 -0
  14. data/lib/fogli/event.rb +9 -0
  15. data/lib/fogli/exception.rb +15 -0
  16. data/lib/fogli/facebook_graph.rb +125 -0
  17. data/lib/fogli/facebook_object.rb +225 -0
  18. data/lib/fogli/facebook_object/connection_proxy.rb +80 -0
  19. data/lib/fogli/facebook_object/connection_scope.rb +123 -0
  20. data/lib/fogli/facebook_object/connections.rb +56 -0
  21. data/lib/fogli/facebook_object/properties.rb +81 -0
  22. data/lib/fogli/facebook_object/scope_methods.rb +49 -0
  23. data/lib/fogli/group.rb +8 -0
  24. data/lib/fogli/link.rb +8 -0
  25. data/lib/fogli/named_object.rb +8 -0
  26. data/lib/fogli/note.rb +7 -0
  27. data/lib/fogli/oauth.rb +167 -0
  28. data/lib/fogli/page.rb +15 -0
  29. data/lib/fogli/photo.rb +8 -0
  30. data/lib/fogli/post.rb +18 -0
  31. data/lib/fogli/status.rb +7 -0
  32. data/lib/fogli/user.rb +36 -0
  33. data/lib/fogli/util/module_attributes.rb +61 -0
  34. data/lib/fogli/util/options.rb +32 -0
  35. data/lib/fogli/video.rb +7 -0
  36. data/test/fogli/facebook_graph_test.rb +124 -0
  37. data/test/fogli/facebook_object/connection_proxy_test.rb +42 -0
  38. data/test/fogli/facebook_object/connection_scope_test.rb +91 -0
  39. data/test/fogli/facebook_object/connections_test.rb +50 -0
  40. data/test/fogli/facebook_object/properties_test.rb +79 -0
  41. data/test/fogli/facebook_object/scope_methods_test.rb +69 -0
  42. data/test/fogli/facebook_object_test.rb +178 -0
  43. data/test/fogli/oauth_test.rb +56 -0
  44. data/test/fogli/post_test.rb +18 -0
  45. data/test/fogli/user_test.rb +25 -0
  46. data/test/fogli/util/options_test.rb +38 -0
  47. data/test/fogli_test.rb +16 -0
  48. data/test/test_helper.rb +14 -0
  49. 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