pius-ruby-satisfaction 0.3.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.
- data/.gemified +12 -0
- data/.gitignore +3 -0
- data/CONTRIBUTORS.txt +7 -0
- data/License.txt +20 -0
- data/README.txt +1 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/satisfaction/associations.rb +20 -0
- data/lib/satisfaction/cache/hash.rb +14 -0
- data/lib/satisfaction/cache/memcache.rb +17 -0
- data/lib/satisfaction/company.rb +18 -0
- data/lib/satisfaction/external_dependencies.rb +11 -0
- data/lib/satisfaction/has_satisfaction.rb +8 -0
- data/lib/satisfaction/identity_map.rb +21 -0
- data/lib/satisfaction/loader.rb +115 -0
- data/lib/satisfaction/person.rb +24 -0
- data/lib/satisfaction/product.rb +17 -0
- data/lib/satisfaction/reply.rb +13 -0
- data/lib/satisfaction/resource/attributes.rb +67 -0
- data/lib/satisfaction/resource.rb +208 -0
- data/lib/satisfaction/search.rb +23 -0
- data/lib/satisfaction/tag.rb +14 -0
- data/lib/satisfaction/topic.rb +20 -0
- data/lib/satisfaction/util.rb +13 -0
- data/lib/satisfaction/version.rb +9 -0
- data/lib/satisfaction.rb +162 -0
- data/spec/company_spec.rb +8 -0
- data/spec/identity_map_spec.rb +28 -0
- data/spec/search_spec.rb +24 -0
- data/spec/spec_helper.rb +9 -0
- metadata +88 -0
data/.gemified
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
---
|
2
|
+
:author: Scott Fleckenstein
|
3
|
+
:dependencies:
|
4
|
+
- rspec
|
5
|
+
- oauth
|
6
|
+
- memcache-client
|
7
|
+
- "activesupport"
|
8
|
+
:version: 0.2.0
|
9
|
+
:name: ruby-satisfaction
|
10
|
+
:summary: Helper gem for the getsatisfaction.com API
|
11
|
+
:email: nullstyle@gmail.com
|
12
|
+
:homepage: http://nullstyle.com
|
data/.gitignore
ADDED
data/CONTRIBUTORS.txt
ADDED
data/License.txt
ADDED
@@ -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.
|
data/README.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
README
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
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/pius/ruby-satisfaction"
|
9
|
+
gemspec.authors = ["Scott Fleckenstein", "Josh Nichols", "Pius Uzamere"]
|
10
|
+
VERSION = '0.3.0'
|
11
|
+
end
|
12
|
+
rescue LoadError
|
13
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
14
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'satisfaction'
|
@@ -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,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,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,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.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.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,23 @@
|
|
1
|
+
class Sfn::Search
|
2
|
+
attr_reader :results, :root_url, :loader
|
3
|
+
|
4
|
+
def initialize(root_url, loader)
|
5
|
+
@root_url = root_url
|
6
|
+
@loader = loader
|
7
|
+
@results = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def for_likely_matches_to(name, entities = %w(companies products))
|
11
|
+
@results = entities.inject({}) {|hash, entity|
|
12
|
+
query_string = "#{@root_url}/#{entity}.json?q=#{name}"
|
13
|
+
#result = Net::HTTP.get_response(URI.parse(query_string)).body
|
14
|
+
answer = @loader.get(query_string)
|
15
|
+
if answer[0] == :ok
|
16
|
+
result = answer[1]
|
17
|
+
hash.merge({entity => JSON.parse(result)})
|
18
|
+
else
|
19
|
+
raise "Search service not available at the moment, please try again later."
|
20
|
+
end
|
21
|
+
}
|
22
|
+
end
|
23
|
+
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
|
data/lib/satisfaction.rb
ADDED
@@ -0,0 +1,162 @@
|
|
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
|
+
require 'satisfaction/search'
|
15
|
+
|
16
|
+
|
17
|
+
# =============
|
18
|
+
# = Resources =
|
19
|
+
# =============
|
20
|
+
|
21
|
+
require 'satisfaction/company'
|
22
|
+
require 'satisfaction/person'
|
23
|
+
require 'satisfaction/topic'
|
24
|
+
require 'satisfaction/tag'
|
25
|
+
require 'satisfaction/product'
|
26
|
+
require 'satisfaction/reply'
|
27
|
+
|
28
|
+
# =============
|
29
|
+
|
30
|
+
include Associations
|
31
|
+
|
32
|
+
attr_reader :options
|
33
|
+
attr_reader :loader
|
34
|
+
attr_reader :consumer
|
35
|
+
attr_reader :token
|
36
|
+
attr_reader :identity_map
|
37
|
+
attr_reader :search
|
38
|
+
|
39
|
+
|
40
|
+
def initialize(options={})
|
41
|
+
@options = options.reverse_merge({
|
42
|
+
:root => "http://api.getsatisfaction.com",
|
43
|
+
:autoload => false,
|
44
|
+
:request_token_url => 'http://getsatisfaction.com/api/request_token',
|
45
|
+
:access_token_url => 'http://getsatisfaction.com/api/access_token',
|
46
|
+
:authorize_url => 'http://getsatisfaction.com/api/authorize',
|
47
|
+
})
|
48
|
+
@loader = Sfn::Loader.new
|
49
|
+
@identity_map = Sfn::IdentityMap.new
|
50
|
+
@search = Sfn::Search.new(options[:root], @loader)
|
51
|
+
|
52
|
+
has_many :companies, :url => '/companies'
|
53
|
+
has_many :people, :url => '/people'
|
54
|
+
has_many :topics, :url => '/topics'
|
55
|
+
has_many :replies, :url => '/replies'
|
56
|
+
has_many :tags, :url => '/tags'
|
57
|
+
has_many :products, :url => '/products'
|
58
|
+
end
|
59
|
+
|
60
|
+
def satisfaction
|
61
|
+
self
|
62
|
+
end
|
63
|
+
|
64
|
+
def me
|
65
|
+
me = satisfaction.identity_map.get_record(Me, 'me') do
|
66
|
+
Sfn::Me.new('me', satisfaction)
|
67
|
+
end
|
68
|
+
|
69
|
+
if me.loaded?
|
70
|
+
me
|
71
|
+
else
|
72
|
+
me.load
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def autoload?
|
77
|
+
options[:autoload]
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_basic_auth(user, password)
|
81
|
+
identity_map.expire_record(Me, 'me')
|
82
|
+
@user = user
|
83
|
+
@password = password
|
84
|
+
end
|
85
|
+
|
86
|
+
def set_consumer(key, secret)
|
87
|
+
identity_map.expire_record(Me, 'me')
|
88
|
+
@consumer = OAuth::Consumer.new(key, secret)
|
89
|
+
end
|
90
|
+
|
91
|
+
def set_token(token, secret)
|
92
|
+
identity_map.expire_record(Me, 'me')
|
93
|
+
@token = OAuth::Token.new(token, secret)
|
94
|
+
end
|
95
|
+
|
96
|
+
def request_token
|
97
|
+
result, body = *@loader.get("#{options[:request_token_url]}", :force => true, :consumer => @consumer, :token => nil)
|
98
|
+
raise "Could not retrieve request token" unless result == :ok
|
99
|
+
response = CGI.parse(body)
|
100
|
+
OAuth::Token.new(response["oauth_token"], response["oauth_token_secret"])
|
101
|
+
end
|
102
|
+
|
103
|
+
def authorize_url(token)
|
104
|
+
"#{options[:authorize_url]}?oauth_token=#{token.token}"
|
105
|
+
end
|
106
|
+
|
107
|
+
def access_token(token)
|
108
|
+
result, body = *@loader.get("#{options[:access_token_url]}", :force => true, :consumer => @consumer, :token => token)
|
109
|
+
raise "Could not retrieve access token" unless result == :ok
|
110
|
+
response = CGI.parse(body)
|
111
|
+
OAuth::Token.new(response["oauth_token"], response["oauth_token_secret"])
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
def url(path, query_string={})
|
116
|
+
qs = query_string.map{|kv| URI.escape(kv.first.to_s) + "=" + URI.escape(kv.last.to_s)}.join("&")
|
117
|
+
URI.parse("#{@options[:root]}#{path}?#{qs}")
|
118
|
+
end
|
119
|
+
|
120
|
+
def get(path, query_string={})
|
121
|
+
url = self.url(path, query_string)
|
122
|
+
|
123
|
+
@loader.get(url, :consumer => @consumer, :token => @token, :user => @user, :password => @password)
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
def post(path, form={})
|
128
|
+
url = self.url(path)
|
129
|
+
@loader.post(url,
|
130
|
+
:consumer => @consumer,
|
131
|
+
:token => @token,
|
132
|
+
:user => @user,
|
133
|
+
:password => @password,
|
134
|
+
:form => form)
|
135
|
+
end
|
136
|
+
|
137
|
+
def delete(path)
|
138
|
+
url = self.url(path)
|
139
|
+
@loader.post(url,
|
140
|
+
:consumer => @consumer,
|
141
|
+
:token => @token,
|
142
|
+
:user => @user,
|
143
|
+
:password => @password,
|
144
|
+
:method => :delete)
|
145
|
+
end
|
146
|
+
|
147
|
+
def put(path, form={})
|
148
|
+
url = self.url(path)
|
149
|
+
@loader.post(url,
|
150
|
+
:consumer => @consumer,
|
151
|
+
:token => @token,
|
152
|
+
:user => @user,
|
153
|
+
:password => @password,
|
154
|
+
:method => :put,
|
155
|
+
:form => form)
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
def validate_options
|
160
|
+
raise ArgumentError, "You must specify a location for the API's service root" if options[:root].blank?
|
161
|
+
end
|
162
|
+
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
|
data/spec/search_spec.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe "Searching" do
|
4
|
+
it "should not blow up" do
|
5
|
+
results = @satisfaction.search.for_likely_matches_to('Cyberdyne')
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should be a well-formed hash with default keys" do
|
9
|
+
results = @satisfaction.search.for_likely_matches_to('Cyberdyne')
|
10
|
+
results.should_not be_empty
|
11
|
+
results.class.should == Hash
|
12
|
+
results.keys.should include('products')
|
13
|
+
results.keys.should include('companies')
|
14
|
+
#raise results.inspect
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should put the results into a set of arrays keyed by category" do
|
18
|
+
results = @satisfaction.search.for_likely_matches_to('Cyberdyne')
|
19
|
+
|
20
|
+
c = results['companies']
|
21
|
+
c.class.should == Array
|
22
|
+
include('companies')
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -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,88 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pius-ruby-satisfaction
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.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
|
+
|
18
|
+
description: Ruby interface to Get Satisfaction
|
19
|
+
email: scott@getsatisfaction.com
|
20
|
+
executables: []
|
21
|
+
|
22
|
+
extensions: []
|
23
|
+
|
24
|
+
extra_rdoc_files:
|
25
|
+
- README.txt
|
26
|
+
files:
|
27
|
+
- .gemified
|
28
|
+
- .gitignore
|
29
|
+
- CONTRIBUTORS.txt
|
30
|
+
- License.txt
|
31
|
+
- README.txt
|
32
|
+
- Rakefile
|
33
|
+
- VERSION
|
34
|
+
- init.rb
|
35
|
+
- lib/satisfaction.rb
|
36
|
+
- lib/satisfaction/associations.rb
|
37
|
+
- lib/satisfaction/cache/hash.rb
|
38
|
+
- lib/satisfaction/cache/memcache.rb
|
39
|
+
- lib/satisfaction/company.rb
|
40
|
+
- lib/satisfaction/external_dependencies.rb
|
41
|
+
- lib/satisfaction/has_satisfaction.rb
|
42
|
+
- lib/satisfaction/identity_map.rb
|
43
|
+
- lib/satisfaction/loader.rb
|
44
|
+
- lib/satisfaction/person.rb
|
45
|
+
- lib/satisfaction/product.rb
|
46
|
+
- lib/satisfaction/reply.rb
|
47
|
+
- lib/satisfaction/resource.rb
|
48
|
+
- lib/satisfaction/resource/attributes.rb
|
49
|
+
- lib/satisfaction/search.rb
|
50
|
+
- lib/satisfaction/tag.rb
|
51
|
+
- lib/satisfaction/topic.rb
|
52
|
+
- lib/satisfaction/util.rb
|
53
|
+
- lib/satisfaction/version.rb
|
54
|
+
- spec/company_spec.rb
|
55
|
+
- spec/identity_map_spec.rb
|
56
|
+
- spec/search_spec.rb
|
57
|
+
- spec/spec_helper.rb
|
58
|
+
has_rdoc: false
|
59
|
+
homepage: http://github.com/pius/ruby-satisfaction
|
60
|
+
post_install_message:
|
61
|
+
rdoc_options:
|
62
|
+
- --charset=UTF-8
|
63
|
+
require_paths:
|
64
|
+
- lib
|
65
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: "0"
|
70
|
+
version:
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project:
|
80
|
+
rubygems_version: 1.2.0
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Ruby interface to Get Satisfaction
|
84
|
+
test_files:
|
85
|
+
- spec/company_spec.rb
|
86
|
+
- spec/identity_map_spec.rb
|
87
|
+
- spec/search_spec.rb
|
88
|
+
- spec/spec_helper.rb
|