riaction 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Gemfile +4 -0
  2. data/Gemfile.lock +59 -0
  3. data/README +110 -0
  4. data/Rakefile +1 -0
  5. data/lib/riaction.rb +3 -0
  6. data/lib/riaction/iactionable/api.rb +209 -0
  7. data/lib/riaction/iactionable/connection.rb +114 -0
  8. data/lib/riaction/iactionable/error.rb +17 -0
  9. data/lib/riaction/iactionable/objects.rb +6 -0
  10. data/lib/riaction/iactionable/objects/achievement.rb +19 -0
  11. data/lib/riaction/iactionable/objects/awardable.rb +44 -0
  12. data/lib/riaction/iactionable/objects/challenge.rb +18 -0
  13. data/lib/riaction/iactionable/objects/goal.rb +19 -0
  14. data/lib/riaction/iactionable/objects/i_actionable_object.rb +40 -0
  15. data/lib/riaction/iactionable/objects/identifier.rb +11 -0
  16. data/lib/riaction/iactionable/objects/leaderboard.rb +10 -0
  17. data/lib/riaction/iactionable/objects/leaderboard_report.rb +23 -0
  18. data/lib/riaction/iactionable/objects/level.rb +18 -0
  19. data/lib/riaction/iactionable/objects/level_type.rb +10 -0
  20. data/lib/riaction/iactionable/objects/point_type.rb +10 -0
  21. data/lib/riaction/iactionable/objects/profile_level.rb +16 -0
  22. data/lib/riaction/iactionable/objects/profile_points.rb +21 -0
  23. data/lib/riaction/iactionable/objects/profile_summary.rb +24 -0
  24. data/lib/riaction/iactionable/objects/progress.rb +37 -0
  25. data/lib/riaction/iactionable/settings.rb +30 -0
  26. data/lib/riaction/riaction.rb +368 -0
  27. data/lib/riaction/version.rb +3 -0
  28. data/lib/tasks/riaction.rake +101 -0
  29. data/riaction.gemspec +32 -0
  30. data/spec/api_spec.rb +288 -0
  31. data/spec/connection_spec.rb +111 -0
  32. data/spec/riaction_spec.rb +253 -0
  33. data/spec/settings_spec.rb +52 -0
  34. data/spec/spec_helper.rb +1 -0
  35. metadata +153 -0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in riaction.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,59 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ riaction (0.0.1)
5
+ activerecord
6
+ activesupport (~> 2.0)
7
+ faraday
8
+ faraday-stack
9
+ resque
10
+
11
+ GEM
12
+ remote: http://rubygems.org/
13
+ specs:
14
+ activerecord (2.3.14)
15
+ activesupport (= 2.3.14)
16
+ activesupport (2.3.14)
17
+ addressable (2.2.6)
18
+ diff-lcs (1.1.3)
19
+ faraday (0.7.5)
20
+ addressable (~> 2.2.6)
21
+ multipart-post (~> 1.1.3)
22
+ rack (>= 1.1.0, < 2)
23
+ faraday-stack (0.1.3)
24
+ faraday (~> 0.6)
25
+ multi_json (1.0.3)
26
+ multipart-post (1.1.3)
27
+ rack (1.3.5)
28
+ rack-protection (1.1.4)
29
+ rack
30
+ redis (2.2.2)
31
+ redis-namespace (1.0.3)
32
+ redis (< 3.0.0)
33
+ resque (1.19.0)
34
+ multi_json (~> 1.0)
35
+ redis-namespace (~> 1.0.2)
36
+ sinatra (>= 0.9.2)
37
+ vegas (~> 0.1.2)
38
+ rspec (2.7.0)
39
+ rspec-core (~> 2.7.0)
40
+ rspec-expectations (~> 2.7.0)
41
+ rspec-mocks (~> 2.7.0)
42
+ rspec-core (2.7.1)
43
+ rspec-expectations (2.7.0)
44
+ diff-lcs (~> 1.1.2)
45
+ rspec-mocks (2.7.0)
46
+ sinatra (1.3.1)
47
+ rack (~> 1.3, >= 1.3.4)
48
+ rack-protection (~> 1.1, >= 1.1.2)
49
+ tilt (~> 1.3, >= 1.3.3)
50
+ tilt (1.3.3)
51
+ vegas (0.1.8)
52
+ rack (>= 1.0.0)
53
+
54
+ PLATFORMS
55
+ ruby
56
+
57
+ DEPENDENCIES
58
+ riaction!
59
+ rspec (~> 2.6)
data/README ADDED
@@ -0,0 +1,110 @@
1
+ ## Overview ##
2
+
3
+ riaction provides both a ruby wrapper for IActionable's restful API and an "acts-as" style interface for a Rails application's ActiveRecord models to associate them with IActionable profiles and have them drive the logging of game events.
4
+
5
+ ## API Wrapper ##
6
+
7
+ The wrapper for IActionable's API is used internally by the rest of the gem, but also may be used directly if desired. IActionable's API is restful, and this wrapper takes each resource and HTTP verb of that API and wraps them as a method that takes arguments that match to the resource and query or body parameters. Before the wrapper can be instantiated or used it must be pre-initialized with your IActionable credentials and version number (IActionable supports older versions but recommends staying up to date):
8
+
9
+ IActionable::Api.init_settings( :app_key => "12345",
10
+ :api_key => "abcde",
11
+ :version => 3 )
12
+ @api = IActionable::Api.new
13
+
14
+ IActionable's API speaks in JSON, and here those responses are wrapped in simple objects where nesting and variable names are determined by [IActionable's documentation](http://www.http://iactionable.com/api/). For example, here the wrapper is making a call to load a profile summary:
15
+
16
+ profile_summary = @api.get_profile_summary("user", "username", "zortnac", 10)
17
+ profile_summary.display_name # => "Chris Eberz"
18
+ profile_summary.identifiers.first # => instance of IActionable::Objects::Identifier
19
+ profile_summary.identifiers.first.id_type # => "username"
20
+ profile_summary.identifiers.first.id # => "zortnac"
21
+
22
+ ## Using riaction In Rails ##
23
+
24
+ While the API wrapper in riaction can be used directly (and I ought just pull it out as a separate gem), the rest of riaction consists of an "acts-as" style interface for your application's ActiveRecord models that leverages the API wrapper to associate your models with IActionable profiles and to have IActionable event logging be driven by your models' CRUD actions. riaction relies on Resque for tasking all of the requests made to IActionable's service.
25
+
26
+ ### Initializing the API Wrapper ###
27
+
28
+ Just as above, before the wrapper can be used (either directly or by the riaction interface) it needs to be initialized with your IActionable credentials. This can be done in a small rails initializer:
29
+
30
+ I_ACTIONABLE_CREDS = (YAML.load_file("#{::Rails.root.to_s}/config/i_actionable.yml")[::Rails.env]).symbolize_keys!
31
+ IAction::Api.init_settings(I_ACTIONABLE_CREDS)
32
+
33
+ ### Declaring A Model As A Profile ###
34
+
35
+ Models in your application may declare themselves as profiles that exist on IActionable.
36
+
37
+ class User < ActiveRecord::Base
38
+ riaction :profile, :type => :player, :username => :nickname, :custom => :id
39
+ end
40
+
41
+ # == Schema Information
42
+ #
43
+ # Table name: users
44
+ #
45
+ # id :integer(4)
46
+ # nickname :string(255)
47
+
48
+ Here, the class User declares itself as a profile of type "player", identifiable by two of IActionable's supported ID types, username and custom, the values of which are the fields (or any symbol that an instance of the class responds to) nickname and id, respectively. When a class declares itself as an riaction profile, an after_create callback will be added to register that model on IActionable as a profile as the type, and with the identifiers, described in the class.
49
+
50
+ #### Profile Instance Methods ####
51
+
52
+ Classes that declare themselves as IActionable profiles are given instance methods that tie in to the IActionable API, as many uses of the API take a profile as an argument.
53
+
54
+ @api.get_profile_summary("player", "username", "zortnac", 10)
55
+ # is equivalent to the following...
56
+ @user_instance.riaction_profile_summary(10)
57
+
58
+ @api.get_profile_challenges("player", "username", "zortnac", :completed)
59
+ # is equivalent to the following...
60
+ @user_instance.riaction_profile_challenges(:completed)
61
+
62
+ @api.add_profile_identifier("player", "username", "zortnac", "custom", 42)
63
+ # is equivalent to the following...
64
+ @user_instance.riaction_update_profile(:custom)
65
+
66
+ ### Declaring Events ###
67
+
68
+ Models in your application may declare any number of events that they log through IActionable. For each event that is declared the important elements are:
69
+
70
+ 1. The event's name (or key)
71
+ 2. The type of trigger that causes the event to be logged
72
+ 3. The profile under which the event is logged
73
+ 4. Any optional parameters (key-value pairs) that you want to pass
74
+
75
+ ` `
76
+
77
+ class Comment
78
+ belongs_to :user
79
+ belongs_to :post
80
+
81
+ riaction :event, :name => :make_a_comment, :trigger => :create, :profile => :user, :params => {:post => :post_id}
82
+ end
83
+
84
+ # == Schema Information
85
+ #
86
+ # Table name: comments
87
+ #
88
+ # id :integer(4)
89
+ # user_id :integer(4)
90
+ # post_id :integer(4)
91
+
92
+ Here, the name of the event is `make_a_comment`. The trigger for the event, in this case, is `:create`, which will add an after_create callback to log the event to the API.
93
+
94
+ _Note: If the trigger is one of :create, :update, or :destroy, then the appropriate ActiveRecord callback will log the event. If the trigger is anything else, then an instance method is provided to log the event by hand. For example, an argument of `:trigger => :foo` will provide an instance method `trigger_foo!`_
95
+
96
+ The profile that this event will be logged under can be any object whose class declares itself as a profile. Here, the profile is the object returned by the ActiveRecord association `:user`, and we assume is an instance of the User class from above. Lastly, the optional params passed along with the event is the key-value pair `{:post => :post_id}`, where `:post_id` is an ActiveRecord table column.
97
+
98
+ Putting this all together, whenever an instance of the Comment class is created, an event is logged for which the equivalent call to the API might look like this:
99
+
100
+ @api.log_event("player", "username", "zortnac", "make_a_comment", {:post => 33})
101
+
102
+ _Note: If a class both declares itself as a profile and declares one or more events, and wants to refer to itself as the profile for any of those events, use `:trigger => :self`_
103
+
104
+ ## IActionable ##
105
+
106
+ [Visit their website!](http://www.iactionable.com)
107
+
108
+ ## Author ##
109
+
110
+ Christopher Eberz; chris@chriseberz.com; @zortnac
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/riaction.rb ADDED
@@ -0,0 +1,3 @@
1
+ require "riaction/version"
2
+ require 'riaction/iactionable/api.rb'
3
+ require "riaction/riaction"
@@ -0,0 +1,209 @@
1
+ require 'riaction/iactionable/connection.rb'
2
+ require 'riaction/iactionable/settings.rb'
3
+ require 'riaction/iactionable/objects.rb'
4
+
5
+ module IActionable
6
+
7
+ class Api
8
+ attr :connection
9
+ @@settings = nil
10
+
11
+ def initialize
12
+ if @@settings
13
+ @connection = IActionable::Connection.new(@@settings)
14
+ else
15
+ raise IActionable::ConfigError.new("IActionable::Api cannot be initialized without credentials being set in IActionable::Api.init_settings()")
16
+ end
17
+ end
18
+
19
+ def self.init_settings(values)
20
+ @@settings = IActionable::Settings.new(values)
21
+ rescue IActionable::ConfigError => e
22
+ raise e
23
+ end
24
+
25
+ def self.settings
26
+ @@settings
27
+ end
28
+
29
+ # =================
30
+ # = Event Logging =
31
+ # =================
32
+
33
+ def log_event(profile_type, id_type, id, event_key, event_attrs = {})
34
+ response = @connection.request.with_app_key.with_api_key.to("/#{profile_type}/#{id_type}/#{id}/events/#{event_key}").with_params(event_attrs).post
35
+ end
36
+
37
+ # =====================
38
+ # = Profile API calls =
39
+ # =====================
40
+
41
+ def get_profile_summary(profile_type, id_type, id, achievement_count = nil)
42
+ request = @connection.request.with_app_key.to("/#{profile_type}/#{id_type}/#{id}")
43
+ request.with_params(:achievement_count => achievement_count) unless achievement_count.blank?
44
+ response = request.get
45
+ IActionable::Objects::ProfileSummary.new(response)
46
+ end
47
+
48
+ def create_profile(profile_type, id_type, id, display_name = nil)
49
+ request = @connection.request.with_app_key.with_api_key.to("/#{profile_type}/#{id_type}/#{id}")
50
+ request.with_params(:display_name => display_name) unless display_name.blank?
51
+ request.post
52
+ end
53
+ alias_method :update_profile, :create_profile
54
+
55
+ def add_profile_identifier(profile_type, id_type, id, alt_id_type, alt_id)
56
+ @connection.request.with_app_key.with_api_key.to("/#{profile_type}/#{id_type}/#{id}/identifiers/#{alt_id_type}/#{alt_id}").post
57
+ end
58
+
59
+ # ====================
60
+ # = Points API calls =
61
+ # ====================
62
+
63
+ def get_profile_points(profile_type, id_type, id, point_type)
64
+ response = @connection.request.with_app_key.to("/#{profile_type}/#{id_type}/#{id}/points/#{point_type}").get
65
+ IActionable::Objects::ProfilePoints.new(response)
66
+ end
67
+
68
+ def update_profile_points(profile_type, id_type, id, point_type, amount, reason = nil)
69
+ request = @connection.request.with_app_key.with_api_key.to("/#{profile_type}/#{id_type}/#{id}/points/#{point_type}").with_params(:value => amount)
70
+ request.with_params(:description => reason) unless reason.blank?
71
+ response = request.post
72
+ IActionable::Objects::ProfilePoints.new(response)
73
+ end
74
+
75
+ # =========================
76
+ # = Achievement API calls =
77
+ # =========================
78
+
79
+ def get_profile_achievements(profile_type, id_type, id, filter_type = nil)
80
+ request = @connection.request.with_app_key
81
+ case filter_type
82
+ when :completed
83
+ request.to("/#{profile_type}/#{id_type}/#{id}/achievements/Completed")
84
+ response = request.get
85
+ response.map{|achievement_json| IActionable::Objects::Achievement.new(achievement_json)}
86
+ when :available
87
+ request.to("/#{profile_type}/#{id_type}/#{id}/achievements/Available")
88
+ response = request.get
89
+ response.map{|achievement_json| IActionable::Objects::Achievement.new(achievement_json)}
90
+ else
91
+ request.to("/#{profile_type}/#{id_type}/#{id}/achievements")
92
+ response = request.get
93
+ {
94
+ :available => response["Available"].map{|achievement_json| IActionable::Objects::Achievement.new(achievement_json)},
95
+ :completed => response["Completed"].map{|achievement_json| IActionable::Objects::Achievement.new(achievement_json)}
96
+ }
97
+ end
98
+ end
99
+
100
+ def get_achievements()
101
+ response = @connection.request.with_app_key.to("/achievements").get
102
+ response.map{|achievement_json| IActionable::Objects::Achievement.new(achievement_json)}
103
+ rescue NoMethodError => e
104
+ []
105
+ end
106
+
107
+ # ========================
108
+ # = Challenges API calls =
109
+ # ========================
110
+
111
+ def get_profile_challenges(profile_type, id_type, id, filter_type = nil)
112
+ request = @connection.request.with_app_key
113
+ case filter_type
114
+ when :completed
115
+ request.to("/#{profile_type}/#{id_type}/#{id}/challenges/Completed")
116
+ response = request.get
117
+ response.map{|challenge_json| IActionable::Objects::Challenge.new(challenge_json)}
118
+ when :available
119
+ request.to("/#{profile_type}/#{id_type}/#{id}/challenges/Available")
120
+ response = request.get
121
+ response.map{|challenge_json| IActionable::Objects::Challenge.new(challenge_json)}
122
+ else
123
+ request.to("/#{profile_type}/#{id_type}/#{id}/challenges")
124
+ response = request.get
125
+ {
126
+ :available => response["Available"].map{|challenge_json| IActionable::Objects::Challenge.new(challenge_json)},
127
+ :completed => response["Completed"].map{|challenge_json| IActionable::Objects::Challenge.new(challenge_json)}
128
+ }
129
+ end
130
+ end
131
+
132
+ def get_challenges()
133
+ response = @connection.request.with_app_key.to("/challenges").get
134
+ response.map{|challenge_json| IActionable::Objects::Challenge.new(challenge_json)}
135
+ rescue NoMethodError => e
136
+ []
137
+ end
138
+
139
+ # ===================
140
+ # = Goals API calls =
141
+ # ===================
142
+
143
+ def get_profile_goals(profile_type, id_type, id, filter_type = nil)
144
+ request = @connection.request.with_app_key
145
+ case filter_type
146
+ when :completed
147
+ request.to("/#{profile_type}/#{id_type}/#{id}/goals/Completed")
148
+ response = request.get
149
+ response.map{|goal_json| IActionable::Objects::Goal.new(goal_json)}
150
+ when :available
151
+ request.to("/#{profile_type}/#{id_type}/#{id}/goals/Available")
152
+ response = request.get
153
+ response.map{|goal_json| IActionable::Objects::Goal.new(goal_json)}
154
+ else
155
+ request.to("/#{profile_type}/#{id_type}/#{id}/goals")
156
+ response = request.get
157
+ {
158
+ :available => response["Available"].map{|goal_json| IActionable::Objects::Goal.new(goal_json)},
159
+ :completed => response["Completed"].map{|goal_json| IActionable::Objects::Goal.new(goal_json)}
160
+ }
161
+ end
162
+ end
163
+
164
+ def get_goals()
165
+ response = @connection.request.with_app_key.to("/goals").get
166
+ response.map{|goal_json| IActionable::Objects::Goal.new(goal_json)}
167
+ rescue NoMethodError => e
168
+ []
169
+ end
170
+
171
+ # =========================
172
+ # = Leaderboard API calls =
173
+ # =========================
174
+
175
+ def get_leaderboard(profile_type, point_type, leaderboard, page_number=nil, page_count=nil, id=nil, id_type=nil)
176
+ request = @connection.request.with_app_key.to("/#{profile_type}/leaderboards/points/#{point_type}/#{leaderboard}")
177
+ request.with_params(:pageNumber => page_number) unless page_number.blank?
178
+ request.with_params(:pageCount => page_count) unless page_count.blank?
179
+ request.with_params(:id => id) unless id.blank? || id_type.blank?
180
+ request.with_params(:idType => id_type) unless id.blank? || id_type.blank?
181
+ response = request.get
182
+ IActionable::Objects::LeaderboardReport.new(response)
183
+ end
184
+
185
+ # ===================================
186
+ # = Profile Notifications API calls =
187
+ # ===================================
188
+
189
+ def get_profile_notifications(profile_type, id_type, id)
190
+ response = @connection.request.with_app_key.to("/#{profile_type}/#{id_type}/#{id}/notifications").get
191
+ {
192
+ :achievements => {
193
+ :available => response["Achievements"]["Available"].map{|a| IActionable::Objects::Achievement.new(a)},
194
+ :completed => response["Achievements"]["Completed"].map{|a| IActionable::Objects::Achievement.new(a)}
195
+ },
196
+ :challenges => {
197
+ :available => response["Challenges"]["Available"].map{|c| IActionable::Objects::Challenge.new(c)},
198
+ :completed => response["Challenges"]["Completed"].map{|c| IActionable::Objects::Challenge.new(c)}
199
+ },
200
+ :goals => {
201
+ :available => response["Goals"]["Available"].map{|g| IActionable::Objects::Goal.new(g)},
202
+ :completed => response["Goals"]["Completed"].map{|g| IActionable::Objects::Goal.new(g)}
203
+ },
204
+ :levels => response["Levels"].map{|l| IActionable::Objects::Level.new(l)},
205
+ :points => response["Points"].map{|p| IActionable::Objects::ProfilePoints.new(p)}
206
+ }
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,114 @@
1
+ require 'faraday'
2
+ require 'faraday_stack'
3
+ require 'riaction/iactionable/error'
4
+
5
+ module IActionable
6
+ class Request < Struct.new(:path, :params, :headers, :body)
7
+ attr :settings
8
+
9
+ def initialize(settings)
10
+ @settings = settings
11
+ self.path = nil
12
+ self.params = {}
13
+ self.headers = {}
14
+ self.body = {}
15
+ end
16
+
17
+ def to(path)
18
+ self.path = path unless path.nil? || path.empty?
19
+ self
20
+ end
21
+
22
+ def with_api_key
23
+ (self.headers[:Authorization] = @settings.api_key) and self
24
+ end
25
+
26
+ def with_app_key
27
+ (self.params[:appKey] = @settings.app_key) and self
28
+ end
29
+
30
+ def with_params(params={})
31
+ self.params.merge!(params) and self
32
+ end
33
+
34
+ def with_body(body={})
35
+ self.body.merge!(body) and self
36
+ end
37
+ end
38
+
39
+ class Connection
40
+ attr :connection
41
+ attr :settings
42
+ attr :request
43
+ attr_accessor :response
44
+
45
+ def initialize(settings)
46
+ @settings = settings
47
+
48
+ @connection = Faraday.new api_url(@settings.version) do |builder|
49
+ builder.use FaradayStack::ResponseJSON, content_type: 'application/json'
50
+ builder.use Faraday::Response::RaiseError
51
+ builder.use Faraday::Adapter::NetHttp
52
+ end
53
+
54
+ @request = nil
55
+ end
56
+
57
+ def request
58
+ (@request = Request.new(@settings)) and self
59
+ end
60
+
61
+ def get(path=nil, query_params={})
62
+ @request ||= Request.new(@settings)
63
+ @request.to(path).with_params(query_params)
64
+ @response = @connection.get do |req|
65
+ req.headers.merge! @request.headers
66
+ req.url @request.path, @request.params
67
+ end
68
+ @response.body
69
+ rescue Faraday::Error::ClientError => e
70
+ handle_client_error e
71
+ ensure
72
+ @request = nil
73
+ end
74
+
75
+ def post(path=nil, query_params={}, body_params={})
76
+ @request ||= Request.new(@settings)
77
+ @request.to(path).with_params(query_params)
78
+ @response = @connection.post do |req|
79
+ req.headers.merge! @request.headers
80
+ req.url @request.path, @request.params
81
+ req.body = @request.body unless @request.body.empty?
82
+ end
83
+ @response.body
84
+ rescue Exception => e
85
+ handle_client_error e
86
+ ensure
87
+ @request = nil
88
+ end
89
+
90
+ private
91
+
92
+ def method_missing(symbol, *args)
93
+ @request.send(symbol, *args) and self
94
+ rescue NoMethodError => e
95
+ raise e
96
+ end
97
+
98
+ def api_url(version)
99
+ "http://api.iactionable.com/v#{version}/"
100
+ end
101
+
102
+ def handle_client_error(e)
103
+ # http://iactionable.com/api/response-codes/
104
+ case e.response[:status]
105
+ when 400
106
+ raise IActionable::Error::BadRequest.new(e.response)
107
+ when 401
108
+ raise IActionable::Error::Unauthorized.new(e.response)
109
+ when 500
110
+ raise IActionable::Error::Internal.new(e.response)
111
+ end
112
+ end
113
+ end
114
+ end