nullstyle-ruby-satisfaction 0.4.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,3 @@
1
+ tmp/*
2
+ log/*
3
+ pkg
@@ -0,0 +1,7 @@
1
+ John Duff
2
+ Pius Uzamere
3
+
4
+ Awards
5
+ ======
6
+
7
+ John Duff gets the "Getting the ball rolling" achievement award for being the first contributor besides myself.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Scott Fleckenstein
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ README
@@ -0,0 +1,19 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "ruby-satisfaction"
5
+ gemspec.summary = "Ruby interface to Get Satisfaction"
6
+ gemspec.description = "Ruby interface to Get Satisfaction"
7
+ gemspec.email = "scott@getsatisfaction.com"
8
+ gemspec.homepage = "http://github.com/nullstyle/ruby-satisfaction"
9
+ gemspec.authors = ["Scott Fleckenstein", "Josh Nichols", "Pius Uzamere"]
10
+ gemspec.rubyforge_project = "satisfaction"
11
+ gemspec.add_dependency('memcache-client', '>= 1.5.0')
12
+ gemspec.add_dependency('oauth', '>= 0.3.5')
13
+ gemspec.add_dependency('activesupport', '>= 2.3.2')
14
+ end
15
+
16
+ Jeweler::RubyforgeTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 4
3
+ :patch: 0
4
+ :major: 0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'satisfaction'
@@ -0,0 +1,161 @@
1
+ require 'satisfaction/external_dependencies'
2
+ module Sfn
3
+ end
4
+ class Satisfaction
5
+ # ==================
6
+ # = Core Utilities =
7
+ # ==================
8
+ require 'satisfaction/util'
9
+ require 'satisfaction/has_satisfaction'
10
+ require 'satisfaction/associations'
11
+ require 'satisfaction/resource'
12
+ require 'satisfaction/loader'
13
+ require 'satisfaction/identity_map'
14
+
15
+
16
+ # =============
17
+ # = Resources =
18
+ # =============
19
+
20
+ require 'satisfaction/company'
21
+ require 'satisfaction/person'
22
+ require 'satisfaction/topic'
23
+ require 'satisfaction/tag'
24
+ require 'satisfaction/product'
25
+ require 'satisfaction/reply'
26
+
27
+ # =============
28
+
29
+ include Associations
30
+
31
+ attr_reader :options
32
+ attr_reader :loader
33
+ attr_reader :consumer
34
+ attr_reader :token
35
+ attr_reader :identity_map
36
+
37
+
38
+ def initialize(options={})
39
+ @options = options.reverse_merge({
40
+ :root => "http://api.getsatisfaction.com",
41
+ :autoload => false,
42
+ :request_token_url => 'http://getsatisfaction.com/api/request_token',
43
+ :access_token_url => 'http://getsatisfaction.com/api/access_token',
44
+ :authorize_url => 'http://getsatisfaction.com/api/authorize',
45
+ })
46
+ @loader = Sfn::Loader.new
47
+ @identity_map = Sfn::IdentityMap.new
48
+
49
+ has_many :companies, :url => '/companies'
50
+ has_many :people, :url => '/people'
51
+ has_many :topics, :url => '/topics'
52
+ has_many :replies, :url => '/replies'
53
+ has_many :tags, :url => '/tags'
54
+ has_many :products, :url => '/products'
55
+ end
56
+
57
+ def satisfaction
58
+ self
59
+ end
60
+
61
+ def me
62
+ me = satisfaction.identity_map.get_record(Me, 'me') do
63
+ Sfn::Me.new('me', satisfaction)
64
+ end
65
+
66
+ if me.loaded?
67
+ me
68
+ else
69
+ me.load
70
+ end
71
+ end
72
+
73
+ def autoload?
74
+ options[:autoload]
75
+ end
76
+
77
+ def set_basic_auth(user, password)
78
+ identity_map.expire_record(Me, 'me')
79
+ @user = user
80
+ @password = password
81
+ end
82
+
83
+ def set_consumer(key, secret)
84
+ identity_map.expire_record(Me, 'me')
85
+ @consumer = OAuth::Consumer.new(key, secret)
86
+ end
87
+
88
+ def set_token(token, secret)
89
+ identity_map.expire_record(Me, 'me')
90
+ @token = OAuth::Token.new(token, secret)
91
+ end
92
+
93
+ def request_token
94
+ result, body = *@loader.get("#{options[:request_token_url]}", :force => true, :consumer => @consumer, :token => nil)
95
+ raise "Could not retrieve request token" unless result == :ok
96
+ response = CGI.parse(body)
97
+ OAuth::Token.new(response["oauth_token"], response["oauth_token_secret"])
98
+ end
99
+
100
+ def authorize_url(token)
101
+ "#{options[:authorize_url]}?oauth_token=#{token.token}"
102
+ end
103
+
104
+ def access_token(token)
105
+ result, body = *@loader.get("#{options[:access_token_url]}", :force => true, :consumer => @consumer, :token => token)
106
+ raise "Could not retrieve access token" unless result == :ok
107
+ response = CGI.parse(body)
108
+ OAuth::Token.new(response["oauth_token"], response["oauth_token_secret"])
109
+ end
110
+
111
+
112
+ def url(path, query_string={})
113
+ qs = query_string.map{|kv| URI.escape(kv.first.to_s) + "=" + URI.escape(kv.last.to_s)}.join("&")
114
+ uri_string = "#{@options[:root]}#{path}"
115
+ uri_string += "?#{qs}" unless qs.blank?
116
+ URI.parse(uri_string)
117
+ end
118
+
119
+ def get(path, query_string={})
120
+ url = self.url(path, query_string)
121
+
122
+ @loader.get(url, :consumer => @consumer, :token => @token, :user => @user, :password => @password)
123
+
124
+ end
125
+
126
+ def post(path, form={})
127
+ url = self.url(path)
128
+ @loader.post(url,
129
+ :consumer => @consumer,
130
+ :token => @token,
131
+ :user => @user,
132
+ :password => @password,
133
+ :form => form)
134
+ end
135
+
136
+ def delete(path)
137
+ url = self.url(path)
138
+ @loader.post(url,
139
+ :consumer => @consumer,
140
+ :token => @token,
141
+ :user => @user,
142
+ :password => @password,
143
+ :method => :delete)
144
+ end
145
+
146
+ def put(path, form={})
147
+ url = self.url(path)
148
+ @loader.post(url,
149
+ :consumer => @consumer,
150
+ :token => @token,
151
+ :user => @user,
152
+ :password => @password,
153
+ :method => :put,
154
+ :form => form)
155
+ end
156
+
157
+ private
158
+ def validate_options
159
+ raise ArgumentError, "You must specify a location for the API's service root" if options[:root].blank?
160
+ end
161
+ end
@@ -0,0 +1,20 @@
1
+ module Associations
2
+ def has_many(resource, options={})
3
+ class_name = options[:class_name] || "Sfn::#{resource.to_s.classify}"
4
+ eval <<-EOS
5
+ def #{resource}
6
+ @#{resource} ||= Sfn::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] || "Sfn::#{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 Sfn::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] = Sfn::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 Sfn::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] = Sfn::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 Sfn::Company < Sfn::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 => 'Sfn::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 Sfn::HasSatisfaction
2
+ attr_reader :satisfaction
3
+
4
+ def initialize(satisfaction)
5
+ @satisfaction = satisfaction
6
+ end
7
+
8
+ end
@@ -0,0 +1,21 @@
1
+ class Sfn::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
+
18
+ def expire_record(klass, id)
19
+ @records[[klass, id]] = nil
20
+ end
21
+ end
@@ -0,0 +1,115 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+
4
+
5
+ class Sfn::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
+ reset_cache
16
+ end
17
+
18
+ def reset_cache
19
+ @cache = case @options[:cache]
20
+ when :hash then HashCache.new
21
+ when :memcache then MemcacheCache.new(@options[:memcache] || {})
22
+ else
23
+ raise ArgumentError, "Invalid cache spec: #{@options[:cache]}"
24
+ end
25
+ end
26
+
27
+ def get(url, options = {})
28
+ uri = get_uri(url)
29
+ request = Net::HTTP::Get.new(uri.request_uri)
30
+ cache_record = cache.get(uri)
31
+
32
+ if cache_record && !options[:force]
33
+ request["If-None-Match"] = cache_record.etag
34
+ end
35
+
36
+ http = Net::HTTP.new(uri.host, uri.port)
37
+ add_authentication(request, http, options)
38
+ response = execute(http, request)
39
+
40
+ case response
41
+ when Net::HTTPNotModified
42
+ return [:ok, cache_record.body]
43
+ when Net::HTTPSuccess
44
+ cache.put(uri, response)
45
+ [:ok, response.body]
46
+ when Net::HTTPMovedPermanently
47
+ limit = options[:redirect_limit] || 3
48
+ raise ArgumentError, "Too many redirects" unless limit > 0 #TODO: what is a better error here?
49
+ get(response['location'], options.merge(:redirect_limit => limit - 1))
50
+ when Net::HTTPBadRequest
51
+ [:bad_request, response.body]
52
+ when Net::HTTPForbidden
53
+ [:forbidden, response.body]
54
+ when Net::HTTPUnauthorized
55
+ [:unauthorized, response.body]
56
+ else
57
+ raise "Explode: #{response.to_yaml}"
58
+ end
59
+ end
60
+
61
+ def post(url, options)
62
+ uri = get_uri(url)
63
+ form = options[:form] || {}
64
+ method_klass = case options[:method]
65
+ when :put then Net::HTTP::Put
66
+ when :delete then Net::HTTP::Delete
67
+ else
68
+ Net::HTTP::Post
69
+ end
70
+
71
+ request = method_klass.new(uri.request_uri)
72
+
73
+ request.set_form_data(form)
74
+
75
+ http = Net::HTTP.new(uri.host, uri.port)
76
+ add_authentication(request, http, options)
77
+ response = execute(http, request)
78
+
79
+ case response
80
+ when Net::HTTPUnauthorized
81
+ [:unauthorized, response.body]
82
+ when Net::HTTPBadRequest
83
+ [:bad_request, response.body]
84
+ when Net::HTTPForbidden
85
+ [:forbidden, response.body]
86
+ when Net::HTTPSuccess
87
+ [:ok, response.body]
88
+ else
89
+ raise "Explode: #{response.to_yaml}"
90
+ end
91
+ end
92
+
93
+ private
94
+ def execute(http, request)
95
+ http.start{|http| http.request(request) }
96
+ end
97
+
98
+ def get_uri(url)
99
+ case url
100
+ when URI then url
101
+ when String then URI.parse(url)
102
+ else
103
+ raise ArgumentError, "Invalid uri, please use a String or URI object"
104
+ end
105
+ end
106
+
107
+ def add_authentication(request, http, options)
108
+ if options[:user]
109
+ request.basic_auth(options[:user], options[:password])
110
+ elsif options[:consumer]
111
+ request.oauth!(http, options[:consumer], options[:token])
112
+ end
113
+ end
114
+ end
115
+
@@ -0,0 +1,24 @@
1
+ class Sfn::Person < Sfn::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 => 'Sfn::Topic'
12
+ end
13
+ end
14
+
15
+ class Me < Sfn::Person
16
+ def path
17
+ loaded? ? super : "/me"
18
+ end
19
+
20
+ def was_loaded(result)
21
+ @id = self.attributes["id"]
22
+ setup_associations
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ class Sfn::Product < Sfn::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 Sfn::Reply < Sfn::Resource
2
+ attributes :content, :star_count, :topic_id
3
+ attribute :created_at, :type => Time
4
+ attribute :author, :type => Sfn::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,208 @@
1
+ require 'forwardable'
2
+
3
+ class Sfn::Resource < Sfn::HasSatisfaction
4
+ require 'satisfaction/resource/attributes'
5
+ include ::Associations
6
+ include Attributes
7
+ attr_reader :id
8
+ include Sfn::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
+
24
+ if result.first == :ok
25
+ self.attributes = JSON.parse(result.last)
26
+ was_loaded(result.last)
27
+ self
28
+ else
29
+ result
30
+ end
31
+ end
32
+
33
+ def was_loaded(result)
34
+ #override this to augment post-loading behavior
35
+ end
36
+
37
+ def delete
38
+ satisfaction.delete("#{path}.json")
39
+ end
40
+
41
+ def put(attrs)
42
+ params = requestify(attrs, self.class.name.demodulize.underscore)
43
+ result = satisfaction.put("#{path}.json", params)
44
+
45
+ if result.first == :ok
46
+ json = JSON.parse(result.last)
47
+ self.attributes = json
48
+ self
49
+ else
50
+ result
51
+ end
52
+ end
53
+
54
+ def loaded?
55
+ !@attributes.nil?
56
+ end
57
+
58
+ def inspect
59
+ if loaded?
60
+ "<#{self.class.name} #{attributes.map{|k,v| "#{k}: #{v}"}.join(' ') if !attributes.nil?}>"
61
+ else
62
+ "<#{self.class.name} #{path} UNLOADED>"
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ class Sfn::ResourceCollection < Sfn::HasSatisfaction
69
+ attr_reader :klass
70
+ attr_reader :path
71
+ include Sfn::Util
72
+
73
+ def initialize(klass, satisfaction, path)
74
+ super satisfaction
75
+ @klass = klass
76
+ @path = path
77
+ end
78
+
79
+ def page(number, options={})
80
+ Sfn::Page.new(self, number, options)
81
+ end
82
+
83
+ def get(id, options={})
84
+ #options currently ignored
85
+ satisfaction.identity_map.get_record(klass, id) do
86
+ klass.new(id, satisfaction)
87
+ end
88
+ end
89
+
90
+ def post(attrs)
91
+ params = requestify(attrs, klass.name.demodulize.underscore)
92
+ result = satisfaction.post("#{path}.json", params)
93
+
94
+ if result.first == :ok
95
+ json = JSON.parse(result.last)
96
+ id = json["id"]
97
+ obj = klass.new(id, satisfaction)
98
+ obj.attributes = json
99
+ obj
100
+ else
101
+ result
102
+ end
103
+ end
104
+
105
+ def [](*key)
106
+ options = key.extract_options!
107
+ case key.length
108
+ when 1
109
+ get(key, options)
110
+ when 2
111
+ page(key.first, options.merge(:limit => key.last))
112
+ else
113
+ raise ArgumentError, "Invalid Array arguement, only use 2-element array: :first is the page number, :last is the page size"
114
+ end
115
+ end
116
+
117
+ end
118
+
119
+ class Sfn::Page < Sfn::HasSatisfaction
120
+ attr_reader :total
121
+ attr_reader :collection
122
+
123
+ extend Forwardable
124
+ def_delegator :items, :first
125
+ def_delegator :items, :last
126
+ def_delegator :items, :each
127
+ def_delegator :items, :each_with_index
128
+ def_delegator :items, :inject
129
+ def_delegator :items, :reject
130
+ def_delegator :items, :select
131
+ def_delegator :items, :map
132
+ def_delegator :items, :[]
133
+ def_delegator :items, :length
134
+ def_delegator :items, :to_a
135
+ def_delegator :items, :empty?
136
+
137
+ def initialize(collection, page, options={})
138
+ super(collection.satisfaction)
139
+ @collection = collection
140
+ @klass = collection.klass
141
+ @page = page
142
+ @path = collection.path
143
+ @options = options
144
+ @options[:limit] ||= 10
145
+ end
146
+
147
+ # Retrieve the items for this page
148
+ # * Caches
149
+ def items
150
+ load
151
+ @data
152
+ end
153
+
154
+ def loaded?
155
+ !@data.nil?
156
+ end
157
+
158
+ def page_size
159
+ @options[:limit]
160
+ end
161
+
162
+ def next?
163
+ load #this loads the data, we shold probably make load set the ivar instead of items ;)
164
+ last_item = @page * page_size
165
+ @total > last_item
166
+ end
167
+
168
+ def next
169
+ return nil unless next?
170
+ self.class.new(@collection, @page + 1, @options)
171
+ end
172
+
173
+
174
+ def prev?
175
+ @page > 1
176
+ end
177
+
178
+ def prev
179
+ return nil unless prev?
180
+ self.class.new(@collection, @page - 1, @options)
181
+ end
182
+
183
+ def page_count
184
+ result = @total / length
185
+ result += 1 if @total % length != 0
186
+ result
187
+ end
188
+
189
+ def load(force=false)
190
+ return @data if loaded? && !force
191
+
192
+ result = satisfaction.get("#{@path}.json", @options.merge(:page => @page))
193
+
194
+ if result.first == :ok
195
+ json = JSON.parse(result.last)
196
+ @total = json["total"]
197
+
198
+ @data = json["data"].map do |result|
199
+ obj = @klass.decode_sfn(result, satisfaction)
200
+ satisfaction.identity_map.get_record(@klass, obj.id) do
201
+ obj
202
+ end
203
+ end
204
+ else
205
+ result
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,67 @@
1
+ module Sfn::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 Sfn::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 Sfn::Tag < Sfn::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 Sfn::Topic < Sfn::Resource
2
+ attributes :subject, :style, :content, :reply_count, :follower_count, :company_id, :at_sfn
3
+ attribute :last_active_at, :type => Time
4
+ attribute :created_at, :type => Time
5
+ attribute :author, :type => Sfn::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 Sfn::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,75 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{ruby-satisfaction}
5
+ s.version = "0.4.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Scott Fleckenstein", "Josh Nichols", "Pius Uzamere"]
9
+ s.date = %q{2009-07-10}
10
+ s.description = %q{Ruby interface to Get Satisfaction}
11
+ s.email = %q{scott@getsatisfaction.com}
12
+ s.extra_rdoc_files = [
13
+ "README.txt"
14
+ ]
15
+ s.files = [
16
+ ".gitignore",
17
+ "CONTRIBUTORS.txt",
18
+ "License.txt",
19
+ "README.txt",
20
+ "Rakefile",
21
+ "VERSION.yml",
22
+ "init.rb",
23
+ "lib/satisfaction.rb",
24
+ "lib/satisfaction/associations.rb",
25
+ "lib/satisfaction/cache/hash.rb",
26
+ "lib/satisfaction/cache/memcache.rb",
27
+ "lib/satisfaction/company.rb",
28
+ "lib/satisfaction/external_dependencies.rb",
29
+ "lib/satisfaction/has_satisfaction.rb",
30
+ "lib/satisfaction/identity_map.rb",
31
+ "lib/satisfaction/loader.rb",
32
+ "lib/satisfaction/person.rb",
33
+ "lib/satisfaction/product.rb",
34
+ "lib/satisfaction/reply.rb",
35
+ "lib/satisfaction/resource.rb",
36
+ "lib/satisfaction/resource/attributes.rb",
37
+ "lib/satisfaction/tag.rb",
38
+ "lib/satisfaction/topic.rb",
39
+ "lib/satisfaction/util.rb",
40
+ "ruby-satisfaction.gemspec",
41
+ "spec/company_spec.rb",
42
+ "spec/identity_map_spec.rb",
43
+ "spec/spec_helper.rb"
44
+ ]
45
+ s.homepage = %q{http://github.com/nullstyle/ruby-satisfaction}
46
+ s.rdoc_options = ["--charset=UTF-8"]
47
+ s.require_paths = ["lib"]
48
+ s.rubyforge_project = %q{satisfaction}
49
+ s.rubygems_version = %q{1.3.4}
50
+ s.summary = %q{Ruby interface to Get Satisfaction}
51
+ s.test_files = [
52
+ "spec/company_spec.rb",
53
+ "spec/identity_map_spec.rb",
54
+ "spec/spec_helper.rb"
55
+ ]
56
+
57
+ if s.respond_to? :specification_version then
58
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
59
+ s.specification_version = 3
60
+
61
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
62
+ s.add_runtime_dependency(%q<memcache-client>, [">= 1.5.0"])
63
+ s.add_runtime_dependency(%q<oauth>, [">= 0.3.5"])
64
+ s.add_runtime_dependency(%q<activesupport>, [">= 2.3.2"])
65
+ else
66
+ s.add_dependency(%q<memcache-client>, [">= 1.5.0"])
67
+ s.add_dependency(%q<oauth>, [">= 0.3.5"])
68
+ s.add_dependency(%q<activesupport>, [">= 2.3.2"])
69
+ end
70
+ else
71
+ s.add_dependency(%q<memcache-client>, [">= 1.5.0"])
72
+ s.add_dependency(%q<oauth>, [">= 0.3.5"])
73
+ s.add_dependency(%q<activesupport>, [">= 2.3.2"])
74
+ end
75
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Company loader" do
4
+ it "should work" do
5
+ @satisfaction.set_consumer('lmwjv4kzwi27', 'fiei6iv61jnoukaq1aylwd8vcmnkafrs')
6
+ p @satisfaction.request_token
7
+ end
8
+ end
@@ -0,0 +1,28 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe "Identity Map" do
4
+ it "should work single instances" do
5
+ c1 = @satisfaction.companies.get(4)
6
+ c2 = @satisfaction.companies.get(4)
7
+
8
+ c1.object_id.should == c2.object_id
9
+ end
10
+
11
+ it "should load one if the other gets loaded" do
12
+ c1 = @satisfaction.companies.get(4)
13
+ c2 = @satisfaction.companies.get(4)
14
+ c2.should_not be_loaded
15
+
16
+ c1.load
17
+
18
+ c2.should be_loaded
19
+ c2.domain.should == 'satisfaction'
20
+ end
21
+
22
+ it "should work with pages too" do
23
+ c1 = @satisfaction.companies.get(4)
24
+ c2 = @satisfaction.companies.page(1, :q => 'satisfaction').first
25
+
26
+ c1.object_id.should == c2.object_id
27
+ end
28
+ end
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ $:.unshift "#{File.dirname(__FILE__)}/../lib"
4
+
5
+ require 'satisfaction'
6
+
7
+ Spec::Runner.configure do |config|
8
+ config.prepend_before(:each){ @satisfaction = Satisfaction.new(:root => 'http://api.getsatisfaction.com') }
9
+ end
metadata ADDED
@@ -0,0 +1,113 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nullstyle-ruby-satisfaction
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ platform: ruby
6
+ authors:
7
+ - Scott Fleckenstein
8
+ - Josh Nichols
9
+ - Pius Uzamere
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-07-10 00:00:00 -07:00
15
+ default_executable:
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: memcache-client
19
+ type: :runtime
20
+ version_requirement:
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.5.0
26
+ version:
27
+ - !ruby/object:Gem::Dependency
28
+ name: oauth
29
+ type: :runtime
30
+ version_requirement:
31
+ version_requirements: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 0.3.5
36
+ version:
37
+ - !ruby/object:Gem::Dependency
38
+ name: activesupport
39
+ type: :runtime
40
+ version_requirement:
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 2.3.2
46
+ version:
47
+ description: Ruby interface to Get Satisfaction
48
+ email: scott@getsatisfaction.com
49
+ executables: []
50
+
51
+ extensions: []
52
+
53
+ extra_rdoc_files:
54
+ - README.txt
55
+ files:
56
+ - .gitignore
57
+ - CONTRIBUTORS.txt
58
+ - License.txt
59
+ - README.txt
60
+ - Rakefile
61
+ - VERSION.yml
62
+ - init.rb
63
+ - lib/satisfaction.rb
64
+ - lib/satisfaction/associations.rb
65
+ - lib/satisfaction/cache/hash.rb
66
+ - lib/satisfaction/cache/memcache.rb
67
+ - lib/satisfaction/company.rb
68
+ - lib/satisfaction/external_dependencies.rb
69
+ - lib/satisfaction/has_satisfaction.rb
70
+ - lib/satisfaction/identity_map.rb
71
+ - lib/satisfaction/loader.rb
72
+ - lib/satisfaction/person.rb
73
+ - lib/satisfaction/product.rb
74
+ - lib/satisfaction/reply.rb
75
+ - lib/satisfaction/resource.rb
76
+ - lib/satisfaction/resource/attributes.rb
77
+ - lib/satisfaction/tag.rb
78
+ - lib/satisfaction/topic.rb
79
+ - lib/satisfaction/util.rb
80
+ - ruby-satisfaction.gemspec
81
+ - spec/company_spec.rb
82
+ - spec/identity_map_spec.rb
83
+ - spec/spec_helper.rb
84
+ has_rdoc: false
85
+ homepage: http://github.com/nullstyle/ruby-satisfaction
86
+ post_install_message:
87
+ rdoc_options:
88
+ - --charset=UTF-8
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: "0"
96
+ version:
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: "0"
102
+ version:
103
+ requirements: []
104
+
105
+ rubyforge_project: satisfaction
106
+ rubygems_version: 1.2.0
107
+ signing_key:
108
+ specification_version: 3
109
+ summary: Ruby interface to Get Satisfaction
110
+ test_files:
111
+ - spec/company_spec.rb
112
+ - spec/identity_map_spec.rb
113
+ - spec/spec_helper.rb