gamifier 1.0.3

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.
Files changed (41) hide show
  1. data/.gitignore +17 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +60 -0
  5. data/Rakefile +2 -0
  6. data/examples/dev.rb +186 -0
  7. data/examples/preprod.rb +218 -0
  8. data/gamifier.gemspec +24 -0
  9. data/lib/gamifier/collection.rb +26 -0
  10. data/lib/gamifier/dsl/network.rb +33 -0
  11. data/lib/gamifier/dsl/site.rb +134 -0
  12. data/lib/gamifier/dsl.rb +24 -0
  13. data/lib/gamifier/engine.rb +84 -0
  14. data/lib/gamifier/errors.rb +6 -0
  15. data/lib/gamifier/model.rb +212 -0
  16. data/lib/gamifier/models/activity.rb +8 -0
  17. data/lib/gamifier/models/activity_definition.rb +6 -0
  18. data/lib/gamifier/models/group.rb +6 -0
  19. data/lib/gamifier/models/player.rb +20 -0
  20. data/lib/gamifier/models/reward.rb +6 -0
  21. data/lib/gamifier/models/reward_definition.rb +14 -0
  22. data/lib/gamifier/models/site.rb +6 -0
  23. data/lib/gamifier/models/track.rb +6 -0
  24. data/lib/gamifier/models/unit.rb +16 -0
  25. data/lib/gamifier/models/user.rb +6 -0
  26. data/lib/gamifier/version.rb +3 -0
  27. data/lib/gamifier.rb +76 -0
  28. data/spec/integration/dsl_integration_spec.rb +76 -0
  29. data/spec/integration/player_integration_spec.rb +41 -0
  30. data/spec/integration/user_integration_spec.rb +19 -0
  31. data/spec/spec_helper.rb +7 -0
  32. data/spec/spec_integration_helper.rb +51 -0
  33. data/spec/unit/collection_spec.rb +21 -0
  34. data/spec/unit/dsl/network_spec.rb +34 -0
  35. data/spec/unit/dsl/site_spec.rb +54 -0
  36. data/spec/unit/dsl_spec.rb +9 -0
  37. data/spec/unit/engine_spec.rb +135 -0
  38. data/spec/unit/gamifier_spec.rb +60 -0
  39. data/spec/unit/model_spec.rb +182 -0
  40. data/spec/unit/models/player_spec.rb +40 -0
  41. metadata +179 -0
@@ -0,0 +1,134 @@
1
+ module Gamifier
2
+ module DSL
3
+ class Site
4
+ attr_reader :behaviors
5
+ attr_reader :units
6
+ attr_reader :rewards
7
+ attr_reader :missions
8
+ attr_reader :tracks
9
+ attr_reader :source
10
+
11
+ def initialize(*args)
12
+ @source = ::Gamifier::Site.new(*args)
13
+ @behaviors = []
14
+ @units = []
15
+ @rewards = []
16
+ @missions = []
17
+ @tracks = []
18
+ end
19
+
20
+ def engine
21
+ source.engine
22
+ end
23
+
24
+ def behavior(name, &block)
25
+ new_behavior = engine.activity_definitions.build(:name => name)
26
+ behaviors.push new_behavior
27
+
28
+ DSL.eval_with_context(new_behavior, &block)
29
+ end
30
+
31
+ def reward(name, &block)
32
+ new_reward = engine.reward_definitions.build(:name => name)
33
+ rewards.push new_reward
34
+
35
+ DSL.eval_with_context(new_reward, &block)
36
+ end
37
+
38
+ def unit(name, &block)
39
+ new_unit = engine.units.build(:name => name)
40
+ units.push new_unit
41
+
42
+ DSL.eval_with_context(new_unit, &block)
43
+ end
44
+
45
+ def mission(name, &block)
46
+ new_mission = engine.groups.build(:name => name)
47
+ missions.push new_mission
48
+
49
+ DSL.eval_with_context(new_mission, &block)
50
+ end
51
+
52
+ def track(name, &block)
53
+ new_track = engine.tracks.build(:label => name)
54
+ tracks.push new_track
55
+
56
+ DSL.eval_with_context(new_track, &block)
57
+ end
58
+
59
+ def save
60
+ if super
61
+ save_units!
62
+ save_behaviors!
63
+ save_rewards!
64
+ save_missions!
65
+ save_tracks!
66
+ else
67
+ raise Error, "Can't save #{self}"
68
+ end
69
+ end
70
+
71
+ def save_units!
72
+ units.each do |unit|
73
+ Gamifier.logger.info "Saving units..."
74
+ unit.site = self.url
75
+ if gunit = engine.send(unit.class.path).find_by_name(unit.name, :site => unit.site)
76
+ unit._id = gunit._id
77
+ # FIXME: once Badgeville returns the unit _id in the
78
+ # response, remove the next line:
79
+ next
80
+ end
81
+ unit.save || raise(Error, "Can't save #{unit}")
82
+ end
83
+ end
84
+
85
+ def save_behaviors!
86
+ behaviors.each do |behavior|
87
+ Gamifier.logger.info "Saving behaviors..."
88
+ behavior.site_id = self._id
89
+ if gbehavior = engine.send(behavior.class.path).find_by_name(behavior.name, :site => self.url)
90
+ behavior._id = gbehavior._id
91
+ end
92
+ behavior.save || raise(Error, "Can't save #{behavior}")
93
+ end
94
+ end
95
+
96
+ def save_rewards!
97
+ rewards.each do |reward|
98
+ Gamifier.logger.info "Saving rewards..."
99
+ reward.site_id = self._id
100
+ if greward = engine.send(reward.class.path).find_by_name(reward.name, :site => self.url)
101
+ reward._id = greward._id
102
+ end
103
+ reward.save || raise(Error, "Can't save #{reward}")
104
+ end
105
+ end
106
+
107
+ def save_missions!
108
+ missions.each do |mission|
109
+ Gamifier.logger.info "Saving missions..."
110
+ mission.site_id = self._id
111
+ if gmission = engine.send(mission.class.path).find_by_name(mission.name, :site => self.url)
112
+ mission._id = gmission._id
113
+ end
114
+ mission.save || raise(Error, "Can't save #{mission}")
115
+ end
116
+ end
117
+
118
+ def save_tracks!
119
+ tracks.each do |track|
120
+ Gamifier.logger.info "Saving tracks..."
121
+ track.site_id = self._id
122
+ if gtrack = engine.send(track.class.path).find_by_label(track.label, :site => self.url)
123
+ track._id = gtrack._id
124
+ end
125
+ track.save || raise(Error, "Can't save #{track}")
126
+ end
127
+ end
128
+
129
+ def method_missing(*args, &block)
130
+ source.send(*args, &block)
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,24 @@
1
+ require 'gamifier/dsl/network'
2
+ require 'gamifier/dsl/site'
3
+
4
+ module Gamifier
5
+ module DSL
6
+ class << self
7
+ def eval_with_context(new_context, &block)
8
+ new_context.extend DSL
9
+ if block
10
+ new_context.instance_eval(&block)
11
+ end
12
+ new_context
13
+ end
14
+ end
15
+
16
+ def set(key, *args, &block)
17
+ if block
18
+ self.send("#{key}=".to_sym, block)
19
+ else
20
+ self.send("#{key}=".to_sym, *args)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,84 @@
1
+ require 'faraday'
2
+ require 'gamifier/collection'
3
+ require 'json'
4
+ require 'rack/utils'
5
+
6
+ module Gamifier
7
+ class Engine
8
+ MODELS = %w{Activity ActivityDefinition Group Player Reward RewardDefinition Site Track Unit User}
9
+
10
+ attr_accessor :connection
11
+ attr_writer :config
12
+
13
+ def initialize(opts = {})
14
+ opts.each{|k,v| config[k.to_sym] = v}
15
+ raise ArgumentError, "Please configure a :uri and :key first." unless ok?
16
+ @connection = Faraday::Connection.new(:url => uri_to) do |builder|
17
+ builder.use Faraday::Adapter::NetHttp # make http requests with Net::HTTP
18
+ end
19
+ end
20
+
21
+ def config
22
+ @config ||= Gamifier.config.dup
23
+ end
24
+
25
+ def ok?
26
+ !config.values_at(:key, :uri).any?{|k| k.nil? || k.empty?}
27
+ end
28
+
29
+ # Sends an HTTP request corresponding to +method+, on +path+, with
30
+ # any options given in +opts+:
31
+ # +query+:: a hash of query parameters to pass with the request
32
+ # +head+:: a hash of HTTP headers to pass with the request
33
+ # +body+:: the body to send with the request. If you pass a Ruby
34
+ # object, it will be automatically converted into
35
+ # x-www-form-urlencoded by default.
36
+ def transmit(method, path, opts = {})
37
+ res = connection.send(method) do |req|
38
+ req.url [path, "json"].join("."), (opts[:query] || {})
39
+ (opts[:head] || {}).each do |k,v|
40
+ req.headers[k] = v
41
+ end
42
+ req.headers['Content-Type'] ||= 'application/x-www-form-urlencoded'
43
+ req.body = self.class.encode_www_form(opts[:body]) unless opts[:body].nil?
44
+ Gamifier.logger.debug "#{method.to_s.upcase} #{req.inspect}"
45
+ end
46
+ Gamifier.logger.debug "#{res.inspect}"
47
+ body = JSON.parse(res.body) rescue res.body
48
+ case res.status
49
+ when 204
50
+ true
51
+ when 200...300
52
+ body
53
+ when 404
54
+ nil
55
+ when 422
56
+ # Badgeville returns 422 when an entry already exists or is not valid
57
+ [:get, :head].include?(method) ? nil : false
58
+ when 400...500
59
+ raise HTTPClientError, body
60
+ when 500...600
61
+ raise HTTPServerError, body
62
+ else
63
+ raise HTTPError, body
64
+ end
65
+ end
66
+
67
+ def uri_to(path = "")
68
+ URI.join(File.join(::Gamifier.config[:uri], ::Gamifier.config[:key], ""), path)
69
+ end
70
+
71
+ MODELS.each do |model|
72
+ underscored = Gamifier.underscore(model)
73
+ require "gamifier/models/#{underscored}"
74
+ klass = Gamifier.const_get(model)
75
+ define_method(klass.path.to_sym) do
76
+ Collection.new(self, klass)
77
+ end
78
+ end
79
+
80
+ def self.encode_www_form(enum)
81
+ Rack::Utils.build_nested_query(enum)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,6 @@
1
+ module Gamifier
2
+ class Error < StandardError; end
3
+ class HTTPError < Error; end
4
+ class HTTPServerError < HTTPError; end
5
+ class HTTPClientError < HTTPError; end
6
+ end
@@ -0,0 +1,212 @@
1
+ require 'ostruct'
2
+
3
+ module Gamifier
4
+ class Model < OpenStruct
5
+ attr_writer :engine
6
+
7
+ # The engine to use when creating or updating records. Default to
8
+ # the global Gamifier engine if not set via <tt>#engine=</tt>
9
+ # method.
10
+ def engine
11
+ @engine ||= Gamifier.engine
12
+ end
13
+
14
+ def attributes; @table.dup; end
15
+
16
+ def save
17
+ new? ? create : update
18
+ end
19
+
20
+ # Allow to update only the specific attributes given in the +opts+ Hash.
21
+ def update_attributes(opts = {})
22
+ raise Error, "Can't update an object if it has not been saved yet" if new?
23
+ update(opts)
24
+ end
25
+
26
+ def payload_for_submission(opts = {})
27
+ attr_to_keep = opts.empty? ? attributes : opts
28
+
29
+ attr_to_keep = attr_to_keep.reject{|k,v|
30
+ self.class.special_attributes.include?(k.to_sym) || (
31
+ !self.class.mutable_attributes.empty? && !self.class.mutable_attributes.include?(k.to_sym)
32
+ )
33
+ }
34
+
35
+ # Nested hashes are dumped as JSON payload.
36
+ attr_to_keep.each do |k,v|
37
+ attr_to_keep[k] = encode(k.to_sym,v) || case v
38
+ when Hash
39
+ JSON.dump(v)
40
+ # Lazyloaded attributes
41
+ when Proc
42
+ v.call
43
+ else
44
+ v.to_s
45
+ end
46
+ end
47
+
48
+ h = { self.class.container => attr_to_keep }
49
+
50
+ self.class.special_attributes.each do |key|
51
+ h[key] = send(key)
52
+ end
53
+
54
+ h
55
+ end
56
+
57
+ # Overwrite in descendants to specifically encode a key/value pair.
58
+ def encode(key, value)
59
+ return nil
60
+ end
61
+
62
+ def _id
63
+ super || attributes[:id]
64
+ end
65
+
66
+ def destroy
67
+ return true if new?
68
+ engine.transmit :delete, path
69
+ end
70
+
71
+ def replace_if_successful(response)
72
+ if response.kind_of?(Hash)
73
+ response.each{|k,v| send("#{k}=".to_sym, v)}
74
+ self
75
+ else
76
+ response
77
+ end
78
+ end
79
+
80
+ # Returns true if the current object does not exist on the server.
81
+ def new?
82
+ self._id.nil?
83
+ end
84
+
85
+ def path
86
+ [self.class.path, self._id || "undefined"].join("/")
87
+ end
88
+
89
+ # To be included as instance methods into the Collection class used
90
+ # to instantiate and find objects. Add more in descendants if you
91
+ # want to add more methods. See Gamifier::Player for an example.
92
+ module FinderMethods
93
+ # Fetch all the records according to the given query parameters.
94
+ # If a block is given, it will automatically lazy-load all the
95
+ # pages and yield each entry, until all the items have been
96
+ # loaded, or until the user returns from the block.
97
+ def all(params = {}, &block)
98
+ params[:page] ||= 1
99
+ params[:per_page] ||= 50
100
+ res = engine.transmit(:get, path, :query => params)
101
+ if res.kind_of?(Hash)
102
+ entries = res['data'].map{|h| build(h)}
103
+ if block
104
+ # Lazy load all the pages
105
+ entries.each{|entry| yield(entry)}
106
+ params[:page] += 1
107
+ all(params, &block) unless res['paging'].empty? || res['data'].empty?
108
+ end
109
+ entries
110
+ else
111
+ res
112
+ end
113
+ end
114
+
115
+ def find_by(key, value, params = {})
116
+ all(params) do |entry|
117
+ return entry if entry.respond_to?(key) && entry.send(key) == value.to_s
118
+ end
119
+ return nil
120
+ end
121
+
122
+ def find_by_name(name, params = {})
123
+ find_by(:name, name, params)
124
+ end
125
+
126
+ def find_by_label(label, params = {})
127
+ find_by(:label, label, params)
128
+ end
129
+
130
+ def find(key, params = {})
131
+ res = engine.transmit(:get, [path, key].join("/"), :query => params)
132
+ select_first_entry_if_any(res)
133
+ end
134
+
135
+ def find_or_create(key, params = {})
136
+ find(key, params) || build(params).save
137
+ end
138
+
139
+ protected
140
+ # Selects the first entry out of a data array, or out of a single
141
+ # data entry.
142
+ def select_first_entry_if_any(response)
143
+ if response.kind_of?(Hash) && response.has_key?('data')
144
+ entry = [response['data']].flatten[0]
145
+ return nil if entry.nil?
146
+ build(entry)
147
+ else
148
+ response
149
+ end
150
+ end
151
+ end
152
+
153
+ module ClassMethods
154
+ def reset!
155
+ @path = nil
156
+ end
157
+
158
+ # Sets or retrieve the path to use to access the resource on the
159
+ # remote server. By default the path will be the pluralized
160
+ # container name (naive pluralization: container+s).
161
+ def path(value = nil)
162
+ if value.nil?
163
+ @path ||= container+"s"
164
+ else
165
+ @path = value
166
+ end
167
+ end
168
+
169
+ # Declare attributes that must be submitted outside the default
170
+ # container.
171
+ def special_attributes *values
172
+ store_or_get("@special_attributes", values)
173
+ end
174
+
175
+ # Define the attributes that will be sent when creating or
176
+ # updating a model.
177
+ def mutable_attributes *values
178
+ store_or_get("@mutable_attributes", values)
179
+ end
180
+
181
+ def store_or_get(variable, values)
182
+ if values.empty?
183
+ v = instance_variable_get(variable)
184
+ v ||= []
185
+ else
186
+ instance_variable_set(variable, values.map{|v| v.to_sym})
187
+ end
188
+ end
189
+ # Returns the container to use when submitting data. E.g. when
190
+ # submitting user attributes as user[email]=xxx, the container is
191
+ # 'user'. Basically, this is the underscored class name.
192
+ def container
193
+ Gamifier.underscore(name)
194
+ end
195
+ end
196
+
197
+ extend ClassMethods
198
+
199
+ protected
200
+
201
+ def create
202
+ res = engine.transmit :post, self.class.path, :body => payload_for_submission
203
+ replace_if_successful(res)
204
+ end
205
+
206
+ def update(opts = {})
207
+ res = engine.transmit :put, path, :body => payload_for_submission(opts)
208
+ replace_if_successful(res)
209
+ end
210
+ end
211
+
212
+ end
@@ -0,0 +1,8 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Activity < Model
5
+ path "activities"
6
+ special_attributes :player_id, :site, :email
7
+ end
8
+ end
@@ -0,0 +1,6 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class ActivityDefinition < Model
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Group < Model
5
+ end
6
+ end
@@ -0,0 +1,20 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Player < Model
5
+
6
+ special_attributes :email, :site
7
+ mutable_attributes :first_name, :last_name, :nickname, :display_name, :picture_url, :admin
8
+
9
+ module FinderMethods
10
+ def find_by_site_and_email(site, email, params = {})
11
+ res = engine.transmit(:get, path, :query => params.merge({:site => site, :email => email}))
12
+ select_first_entry_if_any(res)
13
+ end
14
+ end
15
+
16
+ def credit(verb, metadata = {})
17
+ engine.activities.build(metadata.merge({:player_id => _id, :verb => verb})).save
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,6 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Reward < Model
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class RewardDefinition < Model
5
+ def encode(key, value)
6
+ case key
7
+ when :components
8
+ JSON.dump(value)
9
+ else
10
+ super(key,value)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,6 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Site < Model
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Track < Model
5
+ end
6
+ end
@@ -0,0 +1,16 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class Unit < Model
5
+ special_attributes :site, :type
6
+ mutable_attributes :name, :label, :abbreviation
7
+
8
+ def initialize(*args)
9
+ super
10
+ end
11
+
12
+ def type
13
+ "point"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ require 'gamifier/model'
2
+
3
+ module Gamifier
4
+ class User < Model
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Gamifier
2
+ VERSION = "1.0.3" unless defined?(Gamifier::VERSION)
3
+ end
data/lib/gamifier.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'logger'
2
+ require "gamifier/version"
3
+ require 'gamifier/errors'
4
+
5
+ module Gamifier
6
+ class << self
7
+ attr_writer :logger
8
+
9
+ # Sets a configuration option. The keys will be symbolized.
10
+ def set(key, opts = nil)
11
+ if opts.nil?
12
+ key.each do |k,v|
13
+ config[k.to_sym] = v
14
+ end
15
+ else
16
+ config[key.to_sym] = opts
17
+ end
18
+ end
19
+
20
+ # Returns the configuration Hash.
21
+ def config
22
+ @config ||= {}
23
+ end
24
+
25
+ # Returns the global Engine. If you need multiple engine with
26
+ # different API keys and/or URIs, you should create a new engine
27
+ # directly with Gamifier::Engine.new
28
+ def engine(&block)
29
+ @engine ||= Engine.new
30
+ block.call(@engine) if block
31
+ @engine
32
+ end
33
+
34
+ # Creates and returns a new DSL::Network object. The given block
35
+ # will be evaluated in the context of the network object.
36
+ #
37
+ # Usage:
38
+ #
39
+ # network = Gamifier.dsl do
40
+ # site 'my-site' do
41
+ # set :url, 'my-site.com'
42
+ # ...
43
+ # end
44
+ # end
45
+ def dsl(&block)
46
+ network = DSL::Network.new
47
+ network.instance_eval(&block)
48
+ network
49
+ end
50
+
51
+ def logger
52
+ @logger ||= begin
53
+ l = Logger.new(STDERR)
54
+ l.level = Logger::WARN
55
+ l
56
+ end
57
+ end
58
+
59
+ def reset!
60
+ @engine = nil
61
+ config.clear
62
+ end
63
+
64
+ def underscore(camel_cased_word)
65
+ camel_cased_word.to_s.gsub(/::/, '/').
66
+ gsub("Gamifier/", '').
67
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
68
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
69
+ tr("-", "_").
70
+ downcase
71
+ end
72
+ end
73
+ end
74
+
75
+ require 'gamifier/engine'
76
+ require 'gamifier/dsl'