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