ruby-satisfaction 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+