nike_v2_neura 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/Gemfile +2 -0
- data/README.md +100 -0
- data/Rakefile +13 -0
- data/lib/ext/core_ext.rb +18 -0
- data/lib/nike_v2.rb +33 -0
- data/lib/nike_v2/activities.rb +102 -0
- data/lib/nike_v2/activity.rb +73 -0
- data/lib/nike_v2/base.rb +17 -0
- data/lib/nike_v2/experience_type.rb +27 -0
- data/lib/nike_v2/gps_data.rb +17 -0
- data/lib/nike_v2/metric.rb +48 -0
- data/lib/nike_v2/metrics.rb +45 -0
- data/lib/nike_v2/person.rb +18 -0
- data/lib/nike_v2/resource.rb +69 -0
- data/lib/nike_v2/summary.rb +40 -0
- data/lib/nike_v2/version.rb +3 -0
- data/nike_v2.gemspec +28 -0
- data/spec/factories/activity_factory.rb +7 -0
- data/spec/factories/person_factory.rb +6 -0
- data/spec/fixtures/nike_api_cassettes/activities.yml +370 -0
- data/spec/fixtures/nike_api_cassettes/activity.yml +153 -0
- data/spec/fixtures/nike_api_cassettes/gps_data.yml +36 -0
- data/spec/fixtures/nike_api_cassettes/summary.yml +36 -0
- data/spec/nike_v2/activites_spec.rb +38 -0
- data/spec/nike_v2/activity_spec.rb +24 -0
- data/spec/nike_v2/base_spec.rb +11 -0
- data/spec/nike_v2/gps_data_spec.rb +15 -0
- data/spec/nike_v2/metric_spec.rb +29 -0
- data/spec/nike_v2/metrics_spec.rb +22 -0
- data/spec/nike_v2/person_spec.rb +22 -0
- data/spec/nike_v2/resource_spec.rb +13 -0
- data/spec/nike_v2/summary_spec.rb +16 -0
- data/spec/spec_helper.rb +16 -0
- metadata +240 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
NikeV2
|
2
|
+
====================
|
3
|
+
|
4
|
+
![Build Status](https://travis-ci.org/fuelxc/nike_v2.png)
|
5
|
+
|
6
|
+
Learn more about the Nike+ API at [https://developer.nike.com/](https://developer.nike.com/).
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
``` bash
|
10
|
+
gem install nike_v2
|
11
|
+
```
|
12
|
+
|
13
|
+
### Comming Soon
|
14
|
+
Once Nike releases the OAuth api this gem will include the ability to fetch the access token too
|
15
|
+
|
16
|
+
## Examples
|
17
|
+
|
18
|
+
In order to utilize the Nike+ API in its current state, you'll need to do the following:
|
19
|
+
* Sign up for a Nike+ account at [http://nikeplus.nike.com/plus/](http://nikeplus.nike.com/plus/)
|
20
|
+
* Log into the developer portal and generate an access token at [https://developer.nike.com/](https://developer.nike.com/)
|
21
|
+
|
22
|
+
``` ruby
|
23
|
+
require 'nike_v2'
|
24
|
+
|
25
|
+
# Initialize a person
|
26
|
+
person = NikeV2::Person.new(access_token: 'a1b2c3d4')
|
27
|
+
# Fetch persons summary
|
28
|
+
person.summary
|
29
|
+
# Fetch a persons activities
|
30
|
+
person.activities
|
31
|
+
# Load more data for an activity
|
32
|
+
activity = person.activities.first
|
33
|
+
activity.fetch_data
|
34
|
+
# Fetch GPS Data for an activity
|
35
|
+
activity.gps_data
|
36
|
+
```
|
37
|
+
|
38
|
+
The activities api allows you to directly pass arguements to the Nike+ api. It also allows you to prefetch the metrics for each activity returned
|
39
|
+
``` ruby
|
40
|
+
#fetch 99 activities
|
41
|
+
person.activities(:count => 99)
|
42
|
+
#prefetch the metrics for activities
|
43
|
+
person.activities(:build_metrics => true)
|
44
|
+
```
|
45
|
+
|
46
|
+
We also smart load the metrics for activities now so you don't have to explicity load them
|
47
|
+
``` ruby
|
48
|
+
person.activities.total_fuel #fetches the metrics if they aren't already loaded
|
49
|
+
=> 394
|
50
|
+
```
|
51
|
+
|
52
|
+
As of version 0.3.0 you can cache calls to the Nike+ V2 api. We use the ApiCache (https://github.com/mloughran/api_cache) gem for this and all options in cache directive are passed to the api_cache config. Setting config.cache false disables the cache
|
53
|
+
``` ruby
|
54
|
+
# config/initializers/nike_v2.rb
|
55
|
+
NikeV2.configure do |config|
|
56
|
+
config.cache = {
|
57
|
+
:cache => 3600
|
58
|
+
}
|
59
|
+
end
|
60
|
+
```
|
61
|
+
|
62
|
+
Possible options:
|
63
|
+
```
|
64
|
+
{
|
65
|
+
:cache => 600, # 10 minutes After this time fetch new data
|
66
|
+
:valid => 86400, # 1 day Maximum time to use old data
|
67
|
+
# :forever is a valid option
|
68
|
+
:period => 60, # 1 minute Maximum frequency to call API
|
69
|
+
:timeout => 5 # 5 seconds API response timeout
|
70
|
+
:fail => # Value returned instead of exception on failure
|
71
|
+
}
|
72
|
+
```
|
73
|
+
## Making it Better
|
74
|
+
|
75
|
+
* Fork the project.
|
76
|
+
* Make your feature addition or bug fix in a new topic branch within your repo.
|
77
|
+
* Add specs for any new or modified functionality.
|
78
|
+
* Commit and push your changes to Github
|
79
|
+
* Send a pull request
|
80
|
+
|
81
|
+
## Inspiration
|
82
|
+
I used Kevin Thompson's NikeApi gem as inspiration. Find it here: https://github.com/kevinthompson/nike_api
|
83
|
+
|
84
|
+
|
85
|
+
## Copyright
|
86
|
+
|
87
|
+
Copyright (c) 2013 SmashTank Apps, LLC.
|
88
|
+
|
89
|
+
This program is free software: you can redistribute it and/or modify
|
90
|
+
it under the terms of the GNU General Public License as published by
|
91
|
+
the Free Software Foundation, either version 3 of the License, or
|
92
|
+
(at your option) any later version.
|
93
|
+
|
94
|
+
This program is distributed in the hope that it will be useful,
|
95
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
96
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
97
|
+
GNU General Public License for more details.
|
98
|
+
|
99
|
+
You should have received a copy of the GNU General Public License
|
100
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rspec/core/rake_task'
|
2
|
+
RSpec::Core::RakeTask.new(:spec)
|
3
|
+
task default: :spec
|
4
|
+
|
5
|
+
require 'rdoc/task'
|
6
|
+
Rake::RDocTask.new do |rdoc|
|
7
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
8
|
+
|
9
|
+
rdoc.rdoc_dir = 'rdoc'
|
10
|
+
rdoc.title = "nike_v2 #{version}"
|
11
|
+
rdoc.rdoc_files.include('README*')
|
12
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
13
|
+
end
|
data/lib/ext/core_ext.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
class String
|
2
|
+
def underscore
|
3
|
+
self.gsub(/::/, '/').
|
4
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
5
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
6
|
+
tr("-", "_").
|
7
|
+
downcase
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Range
|
12
|
+
def intersection(other)
|
13
|
+
raise ArgumentError, 'value must be a Range' unless other.kind_of?(Range)
|
14
|
+
return nil if (self.max < other.begin or other.max < self.begin)
|
15
|
+
[self.begin, other.begin].max..[self.max, other.max].min
|
16
|
+
end
|
17
|
+
alias_method :&, :intersection
|
18
|
+
end
|
data/lib/nike_v2.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'alchemist'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'ext/core_ext'
|
4
|
+
require 'tzinfo'
|
5
|
+
|
6
|
+
module NikeV2
|
7
|
+
def self.configuration
|
8
|
+
@configuration ||= Configuration.new
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.configure
|
12
|
+
yield(configuration) if block_given?
|
13
|
+
end
|
14
|
+
|
15
|
+
class Configuration
|
16
|
+
attr_accessor :cache
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@cache = false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'nike_v2/base'
|
25
|
+
require 'nike_v2/metric'
|
26
|
+
require 'nike_v2/metrics'
|
27
|
+
require 'nike_v2/experience_type'
|
28
|
+
require 'nike_v2/resource'
|
29
|
+
require 'nike_v2/person'
|
30
|
+
require 'nike_v2/activity'
|
31
|
+
require 'nike_v2/activities'
|
32
|
+
require 'nike_v2/summary'
|
33
|
+
require 'nike_v2/gps_data'
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'enumerator'
|
2
|
+
module NikeV2
|
3
|
+
class Activities < Resource
|
4
|
+
include Enumerable
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
API_ARGS = [:offset, :count, :start_date, :end_date]
|
8
|
+
|
9
|
+
def_delegators :@activities_array, :[]=, :<<, :[], :count, :length, :each, :first, :last, :collect
|
10
|
+
def_delegator :@person, :access_token
|
11
|
+
|
12
|
+
API_URL = '/me/sport/activities'
|
13
|
+
|
14
|
+
Metrics::METRIC_TYPES.each do |type|
|
15
|
+
self.class_eval do
|
16
|
+
|
17
|
+
method_var_name = 'total_' + type.downcase
|
18
|
+
instance_variable_set('@' + method_var_name, 0.00)
|
19
|
+
define_method(method_var_name){ ivar = instance_variable_get('@' + method_var_name); ivar ||= sum_of(method_var_name)}
|
20
|
+
|
21
|
+
|
22
|
+
define_method("total_#{type.downcase}_during") do |start_date, end_date, convert = false|
|
23
|
+
@activities_array.reject{|a| ((a.started_at..a.ended_at) & (start_date..end_date)).nil?}.collect{|a| a.send("total_#{type.downcase}_during", start_date, end_date, convert)}.inject(:+)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize(attributes = {})
|
29
|
+
raise "#{self.class} requires a person." unless attributes.keys.include?(:person)
|
30
|
+
@build_metrics = attributes.delete(:build_metrics) || false
|
31
|
+
api_args = extract_api_args(attributes)
|
32
|
+
set_attributes(attributes)
|
33
|
+
@activities_array = []
|
34
|
+
|
35
|
+
|
36
|
+
#TODO: make it pass blocks
|
37
|
+
activities = fetch_data(api_args)
|
38
|
+
if !activities.nil? && !activities['data'].nil?
|
39
|
+
build_activities(activities.delete('data'))
|
40
|
+
super(activities)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def fetch_more
|
45
|
+
unless self.paging['next'].nil? || self.paging['next'] == ''
|
46
|
+
fetch_and_build_activities
|
47
|
+
end
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch_all
|
52
|
+
until (self.paging['next'].nil? || self.paging['next'] == '') do
|
53
|
+
fetch_and_build_activities
|
54
|
+
end
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def paging
|
59
|
+
@paging ||= ''
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
|
65
|
+
def sum_of(method_var_name)
|
66
|
+
self.collect(&:"#{method_var_name}").inject(:+) || 0.00
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_activities(data)
|
70
|
+
if data
|
71
|
+
data.each do |activity|
|
72
|
+
self << NikeV2::Activity.new({:person => self}.merge(activity))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
if @build_metrics
|
76
|
+
self.collect(&:load_data)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
def extract_api_args(args)
|
82
|
+
args.inject({}){|h,a| h[camelize_word(a.first)] = a.last if API_ARGS.include?(a.first); h}
|
83
|
+
end
|
84
|
+
|
85
|
+
def camelize_word(word, first_letter_in_uppercase = false)
|
86
|
+
if !!first_letter_in_uppercase
|
87
|
+
word.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
|
88
|
+
else
|
89
|
+
(word[0].to_s.downcase + camelize_word(word, true)[1..-1]).to_sym
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def fetch_and_build_activities
|
94
|
+
url, query = self.paging['next'].match(/^(.*?)\?(.*)$/)[1,2]
|
95
|
+
query = query.split(/&/).inject({}){|h,item| k, v = item.split(/\=/); h[k] = v;h}
|
96
|
+
activities = fetch_data(query)
|
97
|
+
build_activities(activities.delete('data'))
|
98
|
+
|
99
|
+
@paging = activities.delete('paging')
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'nike_v2/metrics'
|
2
|
+
module NikeV2
|
3
|
+
class Activity < Resource
|
4
|
+
extend Forwardable
|
5
|
+
def_delegator :@person, :access_token
|
6
|
+
|
7
|
+
Metrics::METRIC_TYPES.each do |type|
|
8
|
+
self.class_eval do
|
9
|
+
def_delegator :metrics, "total_#{type.downcase}"
|
10
|
+
def_delegator :metrics, type.downcase
|
11
|
+
def_delegator :metrics, "total_#{type.downcase}_during"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
API_URL = '/me/sport/activities'
|
16
|
+
|
17
|
+
def initialize(attributes = {})
|
18
|
+
raise "#{self.class} requires s person." unless attributes.keys.include?(:person)
|
19
|
+
raise "#{self.class} requires an activityId." unless attributes.keys.include?('activityId')
|
20
|
+
|
21
|
+
build_metrics(attributes)
|
22
|
+
super(attributes)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tz
|
26
|
+
TZInfo::Timezone.get(self.activity_time_zone)
|
27
|
+
end
|
28
|
+
|
29
|
+
def gps_data
|
30
|
+
@gps_data ||= NikeV2::GpsData.new(:activity => self)
|
31
|
+
end
|
32
|
+
|
33
|
+
def metrics
|
34
|
+
load_data unless @metrics.is_a?(NikeV2::Metrics)
|
35
|
+
@metrics
|
36
|
+
end
|
37
|
+
|
38
|
+
def load_data
|
39
|
+
data = fetch_data
|
40
|
+
build_metrics(data)
|
41
|
+
set_attributes(data)
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def started_at
|
47
|
+
@started_at ||= Time.parse(self.start_time.gsub('Z', '-') + '00:00')
|
48
|
+
end
|
49
|
+
|
50
|
+
# some reason activities aren't always complete so we need metrics to figure out how long they are
|
51
|
+
def ended_at
|
52
|
+
@ended_at ||= self.respond_to?(:end_time) ? Time.parse(self.end_time.gsub('Z', '-') + '00:00') : started_at + (metrics.durations.to.seconds).to_f
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_tz(time)
|
56
|
+
if time.respond_to?(:strftime)
|
57
|
+
return Time.parse(time.strftime('%Y-%m-%d %H:%M:%S ' + self.tz.to_s))
|
58
|
+
else
|
59
|
+
return Time.parse(time + ' ' + self.tz.to_s)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
def api_url
|
65
|
+
API_URL + "/#{self.activity_id}"
|
66
|
+
end
|
67
|
+
|
68
|
+
def build_metrics(data)
|
69
|
+
metrics = data.delete('metrics') || []
|
70
|
+
@metrics = metrics.empty? ? nil : NikeV2::Metrics.new(self, metrics)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/nike_v2/base.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class Base
|
3
|
+
def initialize(attributes={})
|
4
|
+
@created_at = attributes.delete('created_at')
|
5
|
+
set_attributes(attributes)
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
def set_attributes(attributes)
|
10
|
+
attributes.each do |(attr, val)|
|
11
|
+
attr = attr.to_s.underscore
|
12
|
+
instance_variable_set("@#{attr}", val)
|
13
|
+
instance_eval "def #{attr}() @#{attr} end"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class ExperienceType
|
3
|
+
TO_S_MAP = {
|
4
|
+
'fuelband' => 'Fuel Band',
|
5
|
+
'npluskinecttrg' => 'Nike+ Kinect',
|
6
|
+
'running' => 'Running'
|
7
|
+
}
|
8
|
+
|
9
|
+
TO_KEY_MAP = TO_S_MAP.invert
|
10
|
+
|
11
|
+
def initialize(code)
|
12
|
+
@code = code
|
13
|
+
end
|
14
|
+
|
15
|
+
def code
|
16
|
+
@code
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
TO_S_MAP[code.downcase]
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.code_from_string(string)
|
24
|
+
TO_KEY_MAP[string]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class GpsData < Resource
|
3
|
+
extend Forwardable
|
4
|
+
def_delegator :@activity, :access_token
|
5
|
+
|
6
|
+
def initialize(attributes = {})
|
7
|
+
raise "#{self.class} requires an activity." unless attributes.keys.include?(:activity)
|
8
|
+
set_attributes(attributes)
|
9
|
+
super(fetch_data)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def api_url
|
14
|
+
self.activity.send(:api_url) + '/gps'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module NikeV2
|
2
|
+
class Metric
|
3
|
+
|
4
|
+
def initialize(activity, data)
|
5
|
+
@activity = activity
|
6
|
+
@unit = data['intervalUnit']
|
7
|
+
@type = data ['metricType']
|
8
|
+
@values = data['values'].collect(&:to_f)
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def type
|
13
|
+
@type
|
14
|
+
end
|
15
|
+
|
16
|
+
def values
|
17
|
+
@values
|
18
|
+
end
|
19
|
+
|
20
|
+
def total
|
21
|
+
@total ||= values.inject(:+)
|
22
|
+
end
|
23
|
+
|
24
|
+
def total_during(start, stop, convert_to_local = false)
|
25
|
+
if convert_to_local
|
26
|
+
start = @activity.to_tz(start)
|
27
|
+
stop = @activity.to_tz(stop)
|
28
|
+
end
|
29
|
+
during(start, stop).collect(&:to_f).inject(:+) rescue 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def during(start, stop)
|
33
|
+
start_point = time_to_index(start)
|
34
|
+
duration = time_to_index(stop) - start_point
|
35
|
+
@values[start_point, duration]
|
36
|
+
end
|
37
|
+
|
38
|
+
def duration
|
39
|
+
@values.length.send(@unit.downcase)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def time_to_index(time)
|
44
|
+
difference = time.to_i - @activity.started_at.to_i
|
45
|
+
difference.s.to.send(@unit.downcase).to_s.to_i
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|