ruby-satisfaction 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.
@@ -0,0 +1,145 @@
1
+ require 'satisfaction/external_dependencies'
2
+
3
+ class Satisfaction
4
+ # ==================
5
+ # = Core Utilities =
6
+ # ==================
7
+ require 'satisfaction/util'
8
+ require 'satisfaction/has_satisfaction'
9
+ require 'satisfaction/associations'
10
+ require 'satisfaction/resource'
11
+ require 'satisfaction/loader'
12
+ require 'satisfaction/identity_map'
13
+
14
+
15
+ # =============
16
+ # = Resources =
17
+ # =============
18
+
19
+ require 'satisfaction/company'
20
+ require 'satisfaction/person'
21
+ require 'satisfaction/topic'
22
+ require 'satisfaction/tag'
23
+ require 'satisfaction/product'
24
+ require 'satisfaction/reply'
25
+
26
+ # =============
27
+
28
+ include Associations
29
+
30
+ attr_reader :options
31
+ attr_reader :loader
32
+ attr_reader :consumer
33
+ attr_reader :token
34
+ attr_reader :identity_map
35
+
36
+
37
+ def initialize(options={})
38
+ @options = options.reverse_merge({
39
+ :root => "http://api.getsatisfaction.com",
40
+ :autoload => false,
41
+ :request_token_url => 'http://getsatisfaction.com/api/request_token',
42
+ :access_token_url => 'http://getsatisfaction.com/api/access_token',
43
+ :authorize_url => 'http://getsatisfaction.com/api/authorize',
44
+ })
45
+ @loader = Satisfaction::Loader.new
46
+ @identity_map = Satisfaction::IdentityMap.new
47
+
48
+ has_many :companies, :url => '/companies'
49
+ has_many :people, :url => '/people'
50
+ has_many :topics, :url => '/topics'
51
+ has_many :replies, :url => '/replies'
52
+ has_many :tags, :url => '/tags'
53
+ has_many :products, :url => '/products'
54
+ end
55
+
56
+ def satisfaction
57
+ self
58
+ end
59
+
60
+ def me
61
+ @me ||= Me.new('me', self)
62
+ @me.load
63
+ @me
64
+ end
65
+
66
+ def autoload?
67
+ options[:autoload]
68
+ end
69
+
70
+ def set_basic_auth(user, password)
71
+ @user = user
72
+ @password = password
73
+ end
74
+
75
+ def set_consumer(key, secret)
76
+ @consumer = OAuth::Consumer.new(key, secret)
77
+ end
78
+
79
+ def set_token(token, secret)
80
+ @token = OAuth::Token.new(token, secret)
81
+ end
82
+
83
+ def request_token
84
+ response = CGI.parse(@loader.get("#{options[:request_token_url]}", :force => true, :consumer => @consumer, :token => nil))
85
+ OAuth::Token.new(response["oauth_token"], response["oauth_token_secret"])
86
+ end
87
+
88
+ def authorize_url(token)
89
+ "#{options[:authorize_url]}?oauth_token=#{token.token}"
90
+ end
91
+
92
+ def access_token(token)
93
+ response = CGI.parse(@loader.get("#{options[:access_token_url]}", :force => true, :consumer => @consumer, :token => token))
94
+ OAuth::Token.new(response["oauth_token"], response["oauth_token_secret"])
95
+ end
96
+
97
+
98
+ def url(path, query_string={})
99
+ qs = query_string.map{|kv| URI.escape(kv.first.to_s) + "=" + URI.escape(kv.last.to_s)}.join("&")
100
+ URI.parse("#{@options[:root]}#{path}?#{qs}")
101
+ end
102
+
103
+ def get(path, query_string={})
104
+ url = self.url(path, query_string)
105
+
106
+ @loader.get(url, :consumer => @consumer, :token => @token, :user => @user, :password => @password)
107
+
108
+ end
109
+
110
+ def post(path, form={})
111
+ url = self.url(path)
112
+ @loader.post(url,
113
+ :consumer => @consumer,
114
+ :token => @token,
115
+ :user => @user,
116
+ :password => @password,
117
+ :form => form)
118
+ end
119
+
120
+ def delete(path)
121
+ url = self.url(path)
122
+ @loader.post(url,
123
+ :consumer => @consumer,
124
+ :token => @token,
125
+ :user => @user,
126
+ :password => @password,
127
+ :method => :delete)
128
+ end
129
+
130
+ def put(path, form={})
131
+ url = self.url(path)
132
+ @loader.post(url,
133
+ :consumer => @consumer,
134
+ :token => @token,
135
+ :user => @user,
136
+ :password => @password,
137
+ :method => :put,
138
+ :form => form)
139
+ end
140
+
141
+ private
142
+ def validate_options
143
+ raise ArgumentError, "You must specify a location for the API's service root" if options[:root].blank?
144
+ end
145
+ end
@@ -0,0 +1,20 @@
1
+ module Associations
2
+ def has_many(resource, options={})
3
+ class_name = options[:class_name] || resource.to_s.classify
4
+ eval <<-EOS
5
+ def #{resource}
6
+ @#{resource} ||= ResourceCollection.new(#{class_name}, self.satisfaction, '#{options[:url]}')
7
+ end
8
+ EOS
9
+ end
10
+
11
+ def belongs_to(resource, options={})
12
+ class_name = options[:class_name] || resource.to_s.classify
13
+ parent_id = options[:parent_attribute] || "#{resource}_id"
14
+ eval <<-EOS
15
+ def #{resource}
16
+ @#{resource} ||= #{class_name}.new(#{parent_id}, self.satisfaction)
17
+ end
18
+ EOS
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ class Satisfaction::Loader::HashCache
2
+ def initialize
3
+ @cached_responses = {}
4
+ end
5
+
6
+ def put(url, response)
7
+ return nil if response["ETag"].blank?
8
+ @cached_responses[url.to_s] = Satisfaction::Loader::CacheRecord.new(url, response["ETag"], response.body)
9
+ end
10
+
11
+ def get(url)
12
+ @cached_responses[url.to_s]
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+
2
+ class Satisfaction::Loader::MemcacheCache
3
+ def initialize(options = {})
4
+ options = options.reverse_merge({:servers => ['127.0.0.1:11211'], :namespace => 'satisfaction', })
5
+ @m = MemCache.new(options.delete(:servers), options)
6
+ end
7
+
8
+ def put(url, response)
9
+ return nil if response["ETag"].blank?
10
+
11
+ @m[url.to_s] = Loader::CacheRecord.new(url, response["ETag"], response.body)
12
+ end
13
+
14
+ def get(url)
15
+ @m[url.to_s]
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ class Company < Resource
2
+
3
+ attributes :domain, :name, :logo, :description
4
+
5
+ def path
6
+ "/companies/#{@id}"
7
+ end
8
+
9
+ def setup_associations
10
+ has_many :people, :url => "#{path}/people"
11
+ has_many :topics, :url => "#{path}/topics"
12
+ has_many :products, :url => "#{path}/products"
13
+ has_many :employees, :url => "#{path}/employees", :class_name => 'Person'
14
+ has_many :tags, :url => "#{path}/tags"
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'active_support'
3
+ require 'hpricot'
4
+ require 'json'
5
+ require 'json/add/rails' #make json play nice with the json rails outputs
6
+ gem('memcache-client')
7
+ require 'memcache'
8
+
9
+ require 'oauth'
10
+ require 'oauth/signature/hmac/sha1'
11
+ require 'oauth/client/net_http'
@@ -0,0 +1,8 @@
1
+ class Satisfaction::HasSatisfaction
2
+ attr_reader :satisfaction
3
+
4
+ def initialize(satisfaction)
5
+ @satisfaction = satisfaction
6
+ end
7
+
8
+ end
@@ -0,0 +1,17 @@
1
+ class Satisfaction::IdentityMap
2
+ attr_reader :records, :pages
3
+
4
+ def initialize
5
+ @records = {}
6
+ @pages = {}
7
+ end
8
+
9
+ def get_record(klass, id, &block)
10
+ result = @records[[klass, id]]
11
+ result ||= begin
12
+ obj = yield(klass, id)
13
+ @records[[klass, id]] = obj
14
+ end
15
+ result
16
+ end
17
+ end
@@ -0,0 +1,105 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+
5
+ class Satisfaction::Loader
6
+ require 'satisfaction/cache/hash'
7
+ require 'satisfaction/cache/memcache'
8
+
9
+ CacheRecord = Struct.new(:url, :etag, :body)
10
+ attr_reader :cache
11
+ attr_reader :options
12
+
13
+ def initialize(options={})
14
+ @options = options.reverse_merge({:cache => :hash})
15
+ @cache = case @options[:cache]
16
+ when :hash then HashCache.new
17
+ when :memcache then MemcacheCache.new(@options[:memcache] || {})
18
+ else
19
+ raise ArgumentError, "Invalid cache spec: #{@options[:cache]}"
20
+ end
21
+ end
22
+
23
+ def get(url, options = {})
24
+ uri = get_uri(url)
25
+ request = Net::HTTP::Get.new(uri.request_uri)
26
+ cache_record = cache.get(uri)
27
+
28
+ if cache_record && !options[:force]
29
+ request["If-None-Match"] = cache_record.etag
30
+ end
31
+
32
+ http = Net::HTTP.new(uri.host, uri.port)
33
+ add_authentication(request, options)
34
+ response = execute(http, request)
35
+
36
+ case response
37
+ when Net::HTTPNotModified
38
+ return cache_record.body
39
+ when Net::HTTPSuccess
40
+ cache.put(uri, response)
41
+ response.body
42
+ when Net::HTTPMovedPermanently
43
+ limit = options[:redirect_limit] || 3
44
+ raise ArgumentError, "Too many redirects" unless limit > 0 #TODO: what is a better error here?
45
+ get(response['location'], options.merge(:redirect_limit => limit - 1))
46
+ else
47
+ raise "Explode: #{response.to_yaml}"
48
+ end
49
+ end
50
+
51
+ def post(url, options)
52
+ uri = get_uri(url)
53
+ form = options[:form] || {}
54
+ method_klass = case options[:method]
55
+ when :put then Net::HTTP::Put
56
+ when :delete then Net::HTTP::Delete
57
+ else
58
+ Net::HTTP::Post
59
+ end
60
+
61
+ request = method_klass.new(uri.request_uri)
62
+
63
+ request.set_form_data(form)
64
+
65
+ http = Net::HTTP.new(uri.host, uri.port)
66
+ add_authentication(request, options)
67
+ response = execute(http, request)
68
+
69
+ case response
70
+ when Net::HTTPUnauthorized
71
+ [:unauthorized, response.body]
72
+ when Net::HTTPBadRequest
73
+ [:bad_request, response.body]
74
+ when Net::HTTPForbidden
75
+ [:forbidden, response.body]
76
+ when Net::HTTPSuccess
77
+ [:ok, response.body]
78
+ else
79
+ raise "Explode: #{response.to_yaml}"
80
+ end
81
+ end
82
+
83
+ private
84
+ def execute(http, request)
85
+ http.start{|http| http.request(request) }
86
+ end
87
+
88
+ def get_uri(url)
89
+ case url
90
+ when URI then url
91
+ when String then URI.parse(url)
92
+ else
93
+ raise ArgumentError, "Invalid uri, please use a String or URI object"
94
+ end
95
+ end
96
+
97
+ def add_authentication(request, options)
98
+ if options[:user]
99
+ request.basic_auth(options[:user], options[:password])
100
+ elsif options[:consumer]
101
+ request.oauth!(http, options[:consumer], options[:token])
102
+ end
103
+ end
104
+ end
105
+
@@ -0,0 +1,25 @@
1
+ class Person < Resource
2
+ attributes :name, :id, :photo, :tagline
3
+
4
+ def path
5
+ "/people/#{@id}"
6
+ end
7
+
8
+ def setup_associations
9
+ has_many :replies, :url => "#{path}/replies"
10
+ has_many :topics, :url => "#{path}/topics"
11
+ has_many :followed_topics, :url => "#{path}/followed/topics", :class_name => 'Topic'
12
+ end
13
+ end
14
+
15
+ class Me < Person
16
+ def path
17
+ "/me"
18
+ end
19
+
20
+ def load
21
+ result = satisfaction.get("#{path}.json")
22
+ self.attributes = JSON.parse(result)
23
+ self
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ class Product < Resource
2
+ attributes :name, :url, :image, :description
3
+ attribute :last_active_at, :type => Time
4
+ attribute :created_at, :type => Time
5
+
6
+ def path
7
+ "/products/#{@id}"
8
+ end
9
+
10
+ def setup_associations
11
+ has_many :topics, :url => "#{path}/topics"
12
+ has_many :people, :url => "#{path}/people"
13
+ has_many :companies, :url => "#{path}/companies"
14
+ has_many :tags, :url => "#{path}/tags"
15
+ end
16
+
17
+ end
@@ -0,0 +1,13 @@
1
+ class Reply < Resource
2
+ attributes :content, :star_count, :topic_id
3
+ attribute :created_at, :type => Time
4
+ attribute :author, :type => Person
5
+
6
+ def path
7
+ "/replies/#{id}"
8
+ end
9
+
10
+ def setup_associations
11
+ belongs_to :topic
12
+ end
13
+ end
@@ -0,0 +1,190 @@
1
+ require 'forwardable'
2
+
3
+ class Resource < Satisfaction::HasSatisfaction
4
+ require 'satisfaction/resource/attributes'
5
+ include ::Associations
6
+ include Attributes
7
+ attr_reader :id
8
+ include Satisfaction::Util
9
+
10
+
11
+ def initialize(id, satisfaction)
12
+ super satisfaction
13
+ @id = id
14
+ setup_associations if respond_to?(:setup_associations)
15
+ end
16
+
17
+ def path
18
+ raise "path not implemented in Resource base class"
19
+ end
20
+
21
+ def load
22
+ result = satisfaction.get("#{path}.json")
23
+ self.attributes = JSON.parse(result)
24
+ self
25
+ end
26
+
27
+ def delete
28
+ satisfaction.delete("#{path}.json")
29
+ end
30
+
31
+
32
+ def put(attrs)
33
+ params = requestify(attrs, self.class.name.underscore)
34
+ result = satisfaction.put("#{path}.json", params)
35
+
36
+ if result.first == :ok
37
+ json = JSON.parse(result.last)
38
+ self.attributes = json
39
+ self
40
+ else
41
+ result
42
+ end
43
+ end
44
+
45
+ def loaded?
46
+ !@attributes.nil?
47
+ end
48
+
49
+ def inspect
50
+ "<#{self.class.name} #{attributes.map{|k,v| "#{k}: #{v}"}.join(' ') if !attributes.nil?}>"
51
+ end
52
+
53
+ end
54
+
55
+ class ResourceCollection < Satisfaction::HasSatisfaction
56
+ attr_reader :klass
57
+ attr_reader :path
58
+ include Satisfaction::Util
59
+
60
+ def initialize(klass, satisfaction, path)
61
+ super satisfaction
62
+ @klass = klass
63
+ @path = path
64
+ end
65
+
66
+ def page(number, options={})
67
+ Page.new(self, number, options)
68
+ end
69
+
70
+ def get(id, options={})
71
+ #options currently ignored
72
+ satisfaction.identity_map.get_record(klass, id) do
73
+ klass.new(id, satisfaction)
74
+ end
75
+ end
76
+
77
+ def post(attrs)
78
+ params = requestify(attrs, klass.name.underscore)
79
+ result = satisfaction.post("#{path}.json", params)
80
+
81
+ if result.first == :ok
82
+ json = JSON.parse(result.last)
83
+ id = json["id"]
84
+ obj = klass.new(id, satisfaction)
85
+ obj.attributes = json
86
+ obj
87
+ else
88
+ result
89
+ end
90
+ end
91
+
92
+ def [](*key)
93
+ options = key.extract_options!
94
+ case key.length
95
+ when 1
96
+ get(key, options)
97
+ when 2
98
+ page(key.first, options.merge(:limit => key.last))
99
+ else
100
+ raise ArgumentError, "Invalid Array arguement, only use 2-element array: :first is the page number, :last is the page size"
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+ class Page < Satisfaction::HasSatisfaction
107
+ attr_reader :total
108
+ attr_reader :collection
109
+
110
+ extend Forwardable
111
+ def_delegator :items, :first
112
+ def_delegator :items, :last
113
+ def_delegator :items, :each
114
+ def_delegator :items, :each_with_index
115
+ def_delegator :items, :inject
116
+ def_delegator :items, :reject
117
+ def_delegator :items, :select
118
+ def_delegator :items, :map
119
+ def_delegator :items, :[]
120
+ def_delegator :items, :length
121
+ def_delegator :items, :to_a
122
+ def_delegator :items, :empty?
123
+
124
+ def initialize(collection, page, options={})
125
+ super(collection.satisfaction)
126
+ @collection = collection
127
+ @klass = collection.klass
128
+ @page = page
129
+ @path = collection.path
130
+ @options = options
131
+ @options[:limit] ||= 10
132
+ end
133
+
134
+ # Retrieve the items for this page
135
+ # * Caches
136
+ def items
137
+ load
138
+ @data
139
+ end
140
+
141
+ def loaded?
142
+ !@data.nil?
143
+ end
144
+
145
+ def page_size
146
+ @options[:limit]
147
+ end
148
+
149
+ def next?
150
+ load #this loads the data, we shold probably make load set the ivar instead of items ;)
151
+ last_item = @page * page_size
152
+ @total > last_item
153
+ end
154
+
155
+ def next
156
+ return nil unless next?
157
+ self.class.new(@collection, @page + 1, @options)
158
+ end
159
+
160
+
161
+ def prev?
162
+ @page > 1
163
+ end
164
+
165
+ def prev
166
+ return nil unless prev?
167
+ self.class.new(@collection, @page - 1, @options)
168
+ end
169
+
170
+ def page_count
171
+ result = @total / length
172
+ result += 1 if @total % length != 0
173
+ result
174
+ end
175
+
176
+ def load(force=false)
177
+ return @data if loaded? && !force
178
+
179
+ results = satisfaction.get("#{@path}.json", @options.merge(:page => @page))
180
+ json = JSON.parse(results)
181
+ @total = json["total"]
182
+
183
+ @data = json["data"].map do |result|
184
+ obj = @klass.decode_sfn(result, satisfaction)
185
+ satisfaction.identity_map.get_record(@klass, obj.id) do
186
+ obj
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,67 @@
1
+ module Resource::Attributes
2
+ def self.included(base)
3
+ base.class_eval do
4
+ extend ClassMethods
5
+ include InstanceMethods
6
+ attr_reader :attributes
7
+ end
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def attributes(*names)
13
+ options = names.extract_options!
14
+
15
+ names.each do |name|
16
+ attribute name, options unless name.blank?
17
+ end
18
+ end
19
+
20
+ def attribute(name, options)
21
+ options.reverse_merge!(:type => 'nil')
22
+ raise "Name can't be empty" if name.blank?
23
+
24
+ class_eval <<-EOS
25
+ def #{name}
26
+ self.load unless self.loaded?
27
+ @#{name} ||= decode_raw_attribute(@attributes['#{name}'], #{options[:type]}) if @attributes
28
+ end
29
+ EOS
30
+ end
31
+
32
+ end
33
+
34
+ module InstanceMethods
35
+ def attributes=(value)
36
+ @attributes = value.with_indifferent_access
37
+ end
38
+
39
+ private
40
+ def decode_raw_attribute(value, type)
41
+ if type.respond_to?(:decode_sfn)
42
+ type.decode_sfn(value, self.satisfaction)
43
+ else
44
+ value
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+
51
+ class Time
52
+ def self.decode_sfn(value, satisfaction)
53
+ parse(value)
54
+ end
55
+ end
56
+
57
+ class Resource
58
+ def self.decode_sfn(value, satisfaction)
59
+ case value
60
+ when Hash
61
+ id = value['id']
62
+ returning(new(id, satisfaction)){|r| r.attributes = value}
63
+ else
64
+ new(value, satisfaction)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,14 @@
1
+ class Tag < Resource
2
+ attributes :name
3
+
4
+ def path
5
+ "/tags/#{@id}"
6
+ end
7
+
8
+ def setup_associations
9
+ has_many :topics, :url => "#{path}/topics"
10
+ has_many :companies, :url => "#{path}/companies"
11
+ has_many :products, :url => "#{path}/products"
12
+ end
13
+
14
+ end
@@ -0,0 +1,20 @@
1
+ class Topic < Resource
2
+ attributes :subject, :style, :content, :reply_count, :follower_count, :company_id
3
+ attribute :last_active_at, :type => Time
4
+ attribute :created_at, :type => Time
5
+ attribute :author, :type => Person
6
+
7
+ def path
8
+ "/topics/#{@id}"
9
+ end
10
+
11
+ def setup_associations
12
+ has_many :replies, :url => "#{path}/replies"
13
+ has_many :people, :url => "#{path}/people"
14
+ has_many :products, :url => "#{path}/products"
15
+ has_many :tags, :url => "#{path}/tags"
16
+
17
+ belongs_to :company
18
+ end
19
+
20
+ end
@@ -0,0 +1,13 @@
1
+ module Satisfaction::Util
2
+ def requestify(parameters, prefix=nil)
3
+ parameters.inject({}) do |results, kv|
4
+ if Hash === kv.last
5
+ results = results.merge(requestify(kv.last, "#{prefix}[#{kv.first}]"))
6
+ else
7
+ results["#{prefix}[#{kv.first}]"] = kv.last
8
+ end
9
+
10
+ results
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ class Satisfaction #:nodoc:
2
+ module VERSION #:nodoc:
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ TINY = 1
6
+
7
+ STRING = [MAJOR, MINOR, TINY].join('.')
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-satisfaction
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Scott Fleckenstein
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-04-09 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ version_requirement:
18
+ version_requirements: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: "0"
23
+ version:
24
+ - !ruby/object:Gem::Dependency
25
+ name: oauth
26
+ version_requirement:
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: "0"
32
+ version:
33
+ - !ruby/object:Gem::Dependency
34
+ name: memcache-client
35
+ version_requirement:
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: "0"
41
+ version:
42
+ - !ruby/object:Gem::Dependency
43
+ name: activesupport
44
+ version_requirement:
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ description:
52
+ email: nullstyle@gmail.com
53
+ executables: []
54
+
55
+ extensions: []
56
+
57
+ extra_rdoc_files: []
58
+
59
+ files:
60
+ - lib/satisfaction
61
+ - lib/satisfaction/associations.rb
62
+ - lib/satisfaction/cache
63
+ - lib/satisfaction/cache/hash.rb
64
+ - lib/satisfaction/cache/memcache.rb
65
+ - lib/satisfaction/company.rb
66
+ - lib/satisfaction/external_dependencies.rb
67
+ - lib/satisfaction/has_satisfaction.rb
68
+ - lib/satisfaction/identity_map.rb
69
+ - lib/satisfaction/loader.rb
70
+ - lib/satisfaction/person.rb
71
+ - lib/satisfaction/product.rb
72
+ - lib/satisfaction/reply.rb
73
+ - lib/satisfaction/resource
74
+ - lib/satisfaction/resource/attributes.rb
75
+ - lib/satisfaction/resource.rb
76
+ - lib/satisfaction/tag.rb
77
+ - lib/satisfaction/topic.rb
78
+ - lib/satisfaction/util.rb
79
+ - lib/satisfaction/version.rb
80
+ - lib/satisfaction.rb
81
+ has_rdoc: false
82
+ homepage: http://nullstyle.com
83
+ post_install_message:
84
+ rdoc_options: []
85
+
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: "0"
99
+ version:
100
+ requirements: []
101
+
102
+ rubyforge_project:
103
+ rubygems_version: 1.0.1
104
+ signing_key:
105
+ specification_version: 2
106
+ summary: Helper gem for the getsatisfaction.com API
107
+ test_files: []
108
+