withings-api 0.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 (61) hide show
  1. data/.gitignore +9 -0
  2. data/.simplecov +5 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +7 -0
  5. data/README.rdoc +84 -0
  6. data/Rakefile +14 -0
  7. data/cucumber.yml +2 -0
  8. data/examples/callback_landing.txt +1 -0
  9. data/examples/create_access_token.rb +62 -0
  10. data/features/get_access_token.feature +70 -0
  11. data/features/get_request_token.feature +68 -0
  12. data/features/measure_getmeas_api.feature +16 -0
  13. data/features/step_definitions/api.rb +113 -0
  14. data/features/support/method_mocker.rb +36 -0
  15. data/features/support/world.rb +34 -0
  16. data/lib/withings-api.rb +19 -0
  17. data/lib/withings-api/api_actions.rb +27 -0
  18. data/lib/withings-api/api_response.rb +23 -0
  19. data/lib/withings-api/consts.rb +12 -0
  20. data/lib/withings-api/errors.rb +23 -0
  21. data/lib/withings-api/oauth_actions.rb +94 -0
  22. data/lib/withings-api/oauth_base.rb +121 -0
  23. data/lib/withings-api/query_string.rb +16 -0
  24. data/lib/withings-api/results/measure_getmeas_results.rb +73 -0
  25. data/lib/withings-api/tokens.rb +35 -0
  26. data/lib/withings-api/types.rb +108 -0
  27. data/lib/withings-api/utils.rb +14 -0
  28. data/lib/withings-api/version.rb +5 -0
  29. data/spec/api_actions/measure_getmeas_spec.rb +22 -0
  30. data/spec/measure_getmeas_results_spec.rb +124 -0
  31. data/spec/method_aliaser_spec.rb +96 -0
  32. data/spec/query_string_spec.rb +20 -0
  33. data/spec/spec_helper.rb +30 -0
  34. data/spec/tokens_spec.rb +38 -0
  35. data/spec/types/atttribution_type_spec.rb +15 -0
  36. data/spec/types/category_type_spec.rb +15 -0
  37. data/spec/types/device_type_spec.rb +15 -0
  38. data/spec/types/measurement_type_spec.rb +15 -0
  39. data/spec/withings_api_spec.rb +67 -0
  40. data/test/README +1 -0
  41. data/test/helpers/http_stubber.rb +32 -0
  42. data/test/helpers/method_aliaser.rb +48 -0
  43. data/test/helpers/stubbed_withings_api.rb +41 -0
  44. data/test/http_stub_responses/access_token/invalid.txt +10 -0
  45. data/test/http_stub_responses/access_token/success.txt +11 -0
  46. data/test/http_stub_responses/access_token/unauthorized_token.txt +11 -0
  47. data/test/http_stub_responses/authorization_callback/success.txt +9 -0
  48. data/test/http_stub_responses/measure_getmeas/forbidden.txt +12 -0
  49. data/test/http_stub_responses/measure_getmeas/oauth_error.txt +9 -0
  50. data/test/http_stub_responses/measure_getmeas/success.txt +9 -0
  51. data/test/http_stub_responses/request_token/invalid_consumer_credentials.txt +10 -0
  52. data/test/http_stub_responses/request_token/success.txt +11 -0
  53. data/test/sample_json/measure_getmeas.json +49 -0
  54. data/test/sample_json/notify_get.json +7 -0
  55. data/test/sample_json/notify_list.json +15 -0
  56. data/test/sample_json/notify_revoke.json +3 -0
  57. data/test/sample_json/notify_subscribe.json +3 -0
  58. data/test/sample_json/once_probe.json +1 -0
  59. data/test/sample_json/user_getbyuserid.json +16 -0
  60. data/withings-api.gemspec +32 -0
  61. metadata +220 -0
@@ -0,0 +1,36 @@
1
+ #
2
+ # ss
3
+ #
4
+
5
+ require_relative "../../test/helpers/method_aliaser"
6
+ require 'net/http'
7
+
8
+ def before_after_method_wrap(clazz, method_sym, &block)
9
+ wrap = nil
10
+
11
+ Before do
12
+ wrap = MethodAliaser.alias_it(clazz, method_sym, &block)
13
+ end
14
+
15
+ After do
16
+ wrap.unalias_it
17
+ end
18
+ end
19
+
20
+ def print_http_req_resp
21
+ before_after_method_wrap(Net::HTTP, :transport_request) do |aliased, *arguments|
22
+ puts "HTTP Request: #{arguments.first.path}"
23
+ res = aliased.call(*arguments)
24
+ puts "HTTP Response:"
25
+ puts "HTTP/#{res.http_version} #{res.code} #{res.message}"
26
+ res.to_hash.each_pair do |key, value|
27
+ puts "#{key}: #{value.join("; ")}"
28
+ end
29
+ puts ""
30
+ puts res.body
31
+
32
+ res
33
+ end
34
+ end
35
+
36
+ print_http_req_resp
@@ -0,0 +1,34 @@
1
+ require 'capybara/cucumber'
2
+
3
+ module ApiCucumberHelpers
4
+ # executes the given block, storing the return value
5
+ # in an instance variable named "name" or the exception
6
+ # result in "name"_exception
7
+ def result_or_exception(name, &block)
8
+ begin
9
+ self.instance_variable_set("@#{name}", yield)
10
+ rescue => e
11
+ puts e
12
+ self.instance_variable_set("@#{name}_exception", e)
13
+ end
14
+ end
15
+
16
+ def logger
17
+ Cucumber.logger
18
+ end
19
+ end
20
+
21
+ World(ApiCucumberHelpers)
22
+
23
+ # Capybara Setup
24
+
25
+ Capybara.default_driver = :selenium
26
+ Capybara.default_wait_time = 5
27
+ World(Capybara::DSL)
28
+
29
+ # overwrite puts to make it's output more
30
+ # harmonious with the stylized Cucumber
31
+ # output
32
+ def puts(o)
33
+ Kernel.puts " \33[36m#{o}\33[0m"
34
+ end
@@ -0,0 +1,19 @@
1
+ require "withings-api/version"
2
+
3
+ require "withings-api/consts"
4
+ require "withings-api/query_string"
5
+ require "withings-api/utils"
6
+ require "withings-api/types"
7
+ require "withings-api/tokens"
8
+ require "withings-api/errors"
9
+ require "withings-api/oauth_base"
10
+ require "withings-api/oauth_actions"
11
+ require "withings-api/api_response"
12
+ require "withings-api/api_actions"
13
+
14
+ module Withings
15
+ module Api
16
+ extend OAuthActions
17
+ extend ApiActions
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ require "withings-api/results/measure_getmeas_results"
2
+
3
+ module Withings
4
+ module Api
5
+ module ApiActions
6
+ include OAuthBase
7
+
8
+ def measure_getmeas(parameters = {}, access_token = nil, consumer_token = nil)
9
+ consumer_token = consumer_token(consumer_token)
10
+ access_token = access_token(access_token)
11
+
12
+ http_response = oauth_http_request!(consumer_token, access_token, {:path => "http://wbsapi.withings.net/measure?action=getmeas", :parameters => parameters})
13
+ Withings::Api::ApiResponse.create!(http_response, Withings::Api::MeasureGetmeasResults)
14
+ end
15
+
16
+ private
17
+
18
+ def consumer_token(o)
19
+ @consumer_token || o || raise(StandardError, "No consumer token")
20
+ end
21
+
22
+ def access_token(o)
23
+ @consumer_token || o || raise(StandardError, "No access token")
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,23 @@
1
+ module Withings::Api
2
+ class ApiResponse
3
+ include ResultsHelpers
4
+
5
+ attr_reader :status, :body
6
+
7
+ def self.create!(http_response, body_class)
8
+ raise HttpNotSuccessError.new(http_response.code, http_response.body) if http_response.code != '200'
9
+
10
+ self.new(http_response.body, body_class)
11
+ end
12
+
13
+ def initialize(string_or_json, body_class)
14
+ hash = coerce_hash string_or_json
15
+
16
+ @status = hash["status"] || raise(InvalidFormat, :status_field_missing)
17
+
18
+ if hash.key?("body")
19
+ @body = body_class.new(hash["body"])
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module Withings
2
+ module Api
3
+ module Defaults
4
+ API_BASE_URL = "http://wbsapi.withings.net"
5
+ OAUTH_BASE_URL = "https://oauth.withings.com"
6
+
7
+ OAUTH_REQUEST_TOKEN_PATH = "/account/request_token"
8
+ OAUTH_AUTHORIZE_PATH = "/account/authorize"
9
+ OAUTH_ACCESS_TOKEN_PATH = "/account/access_token"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ module Withings
2
+ module Api
3
+ class Error < StandardError
4
+
5
+ end
6
+
7
+ class InvalidFormat < Error
8
+
9
+ end
10
+
11
+ class HttpNotSuccessError < Error
12
+ attr_accessor :code, :body
13
+
14
+ def initialize(code, body = "")
15
+ super(code)
16
+
17
+ @code = code
18
+ @body = body
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,94 @@
1
+ require 'oauth'
2
+
3
+ module Withings
4
+ module Api
5
+ module OAuth
6
+ include ::OAuth
7
+ end
8
+
9
+ # Simple API to ease the OAuth setup steps for Withing API client apps.
10
+ #
11
+ # Specifically, this class provides methods for OAuth access token creation.
12
+ #
13
+ # 1. Request request tokens - via {#create_request_token}
14
+ # 1. Redirect to authorization URL (this is handled outside of these methods, ie: by the webapp, etc.)
15
+ # 1. Request access tokens (for permanent access to Withings content) - via {#create_access_token}
16
+ module OAuthActions
17
+ include OAuthBase
18
+
19
+ Defaults = Withings::Api::Defaults
20
+
21
+ # Issues the "request_token" oauth HTTP request to Withings.
22
+ #
23
+ # For call details @ Withings, see http://www.withings.com/en/api/oauthguide#access
24
+ #
25
+ # For details about registering your application with Withings, see http://www.withings.com/en/api/oauthguide#registration
26
+ #
27
+ # @param [String] consumer_token the consumer key Withings assigned on app registration
28
+ # @param [String] consumer_secret the consumer secret Withings assigned on app registration
29
+ # @param [String] callback_url the URL Withings should return the user to after authorization
30
+ #
31
+ # @return [RequestTokenResponse] something encapsulating the request response
32
+ #
33
+ # @raise [Timeout::Error] on connection, or read timeout
34
+ # @raise [SystemCallError] on low level system call errors (connection timeout, connection refused)
35
+ # @raise [ProtocolError] for HTTP 5XX error response codes
36
+ # @raise [OAuth::Unauthorized] for HTTP 4XX error reponse codes
37
+ # @raise [StandardError] for everything else
38
+ def create_request_token(consumer_token, *arguments)
39
+ _consumer_token, _consumer_secret, _callback_url = nil
40
+
41
+ if arguments.length == 1 && consumer_token.instance_of?(Withings::Api::ConsumerToken)
42
+ _consumer_token, _consumer_secret = consumer_token.to_a
43
+ elsif arguments.length == 2
44
+ _consumer_token = consumer_token
45
+ _consumer_secret = arguments.shift
46
+ else
47
+ raise(ArgumentError)
48
+ end
49
+ _callback_url = arguments.shift
50
+
51
+ # TODO: warn if the callback URL isn't HTTPS
52
+ consumer = create_consumer(_consumer_token, _consumer_secret)
53
+ oauth_request_token = consumer.get_request_token({:oauth_callback => _callback_url})
54
+
55
+ RequestTokenResponse.new oauth_request_token
56
+ end
57
+
58
+
59
+ # Issues the "access_token" oauth HTTP request to Withings
60
+ #
61
+ # @param [RequestTokenResponse] request_token request token returned from {#create_request_token}
62
+ # @param [String] user_id user id as returned from Withings via the {RequestTokenResponse#authorization_url}
63
+ #
64
+ # @return [] the shit
65
+ def create_access_token(request_token, *arguments)
66
+ _consumer, _request_token, _user_id = nil
67
+
68
+ if request_token.instance_of?(RequestTokenResponse) && arguments.length == 1
69
+ _consumer = request_token.oauth_consumer
70
+ _request_token = request_token.oauth_request_token
71
+ _user_id = arguments.shift
72
+ elsif request_token.instance_of?(RequestToken) && arguments.length == 2
73
+ request_token.instance_of?(ConsumerToken)
74
+ _consumer = create_consumer(*arguments.shift.to_a)
75
+ _request_token = OAuth::RequestToken.new(_consumer, *request_token.to_a)
76
+ _user_id = arguments.shift
77
+ else
78
+ raise ArgumentError
79
+ end
80
+
81
+ oauth_access_token = _consumer.get_access_token(_request_token)
82
+
83
+ # test for unauthorized token, since oauth + withings doesn't turn this into an explicit
84
+ # error code / exception
85
+ if oauth_access_token.params.key?(:"unauthorized token")
86
+ raise StandardError, :"unauthorized token"
87
+ end
88
+
89
+ AccessTokenResponse.new oauth_access_token
90
+ end
91
+
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,121 @@
1
+ require 'net/http'
2
+
3
+ module Withings::Api
4
+
5
+ module OAuthBase
6
+ private
7
+
8
+ def create_consumer(consumer_key, consumer_secret)
9
+ OAuth::Consumer.new(consumer_key, consumer_secret, {
10
+ # todo, this needs to be parameterized
11
+ :site => Defaults::OAUTH_BASE_URL,
12
+ :scheme => :query_string,
13
+ :http_method => :get,
14
+ :signature_method => 'HMAC-SHA1',
15
+ :request_token_path => Defaults::OAUTH_REQUEST_TOKEN_PATH,
16
+ :authorize_path => Defaults::OAUTH_AUTHORIZE_PATH,
17
+ :access_token_path => Defaults::OAUTH_ACCESS_TOKEN_PATH,
18
+ })
19
+
20
+ end
21
+
22
+ def create_signed_request(consumer_token, access_token, parameters = {})
23
+ default_parameters = {
24
+ :method => :get,
25
+ :parameters => {},
26
+ :headers => {}
27
+ }
28
+
29
+ parameters = default_parameters.merge parameters
30
+
31
+ method = parameters[:method].downcase
32
+ path = parameters[:path]
33
+ http_parameters = parameters[:parameters]
34
+ http_headers = parameters[:headers]
35
+
36
+ if method == :get && !http_parameters.empty?
37
+ query_string = http_parameters.to_query_string
38
+
39
+ path += "?" if ! path.include?("?")
40
+ path += "&" if ! path.end_with? "?"
41
+
42
+ path += query_string
43
+ end
44
+
45
+ consumer = create_consumer(consumer_token.key, consumer_token.secret)
46
+
47
+ _access_token = OAuth::AccessToken.new(consumer, *access_token.to_a)
48
+ if [:post, :put].include?(method)
49
+ consumer.create_signed_request(method, path, _access_token, http_parameters, http_headers)
50
+ else
51
+ consumer.create_signed_request(method, path, _access_token, http_headers)
52
+ end
53
+ end
54
+
55
+ def oauth_http_request!(consumer_token, access_token, parameters = {})
56
+ request = create_signed_request(consumer_token, access_token, parameters)
57
+ Net::HTTP.new("wbsapi.withings.net").request(request)
58
+ end
59
+ end
60
+
61
+ # Simple wrapper class that encapsulates the results of a call to {StaticHelpers#create_request_token}
62
+ class RequestTokenResponse
63
+ def initialize(oauth_request_token)
64
+ self.oauth_request_token = oauth_request_token
65
+ end
66
+
67
+ # @return [String] the OAuth request token key
68
+ def token
69
+ self.oauth_request_token.token
70
+ end
71
+
72
+ alias :key :token
73
+
74
+ # @return [String] the OAuth request token secret
75
+ def secret
76
+ self.oauth_request_token.secret
77
+ end
78
+
79
+ # @return [String] URL to redirect the user to to authorize the access to their data
80
+ def authorization_url
81
+ self.oauth_request_token.authorize_url
82
+ end
83
+
84
+ # @return [RequestToken]
85
+ def request_token
86
+ RequestToken.new(self.key, self.secret)
87
+ end
88
+
89
+ attr_accessor :oauth_request_token
90
+
91
+ # :nodoc:
92
+ def oauth_consumer
93
+ self.oauth_request_token.consumer
94
+ end
95
+ end
96
+
97
+ class AccessTokenResponse
98
+ def initialize(oauth_access_token)
99
+ @oauth_access_token = oauth_access_token
100
+ end
101
+
102
+ def token
103
+ @oauth_access_token.token
104
+ end
105
+
106
+ alias :key :token
107
+
108
+ def secret
109
+ @oauth_access_token.secret
110
+ end
111
+
112
+ def user_id
113
+ @oauth_access_token.params["userid"]
114
+ end
115
+
116
+ def access_token
117
+ AccessToken.new(self.key, self.secret)
118
+ end
119
+ end
120
+
121
+ end
@@ -0,0 +1,16 @@
1
+ require 'uri'
2
+
3
+ class Hash
4
+ QUERY_STRING_RESERVERED = /[\$&\+,\/:;=\?@ <>"#%\{\}\|\\\^~\[\]`]/
5
+
6
+ def to_query_string
7
+ hash = self
8
+
9
+ params = []
10
+ hash.keys.each do |key|
11
+ params << [key, hash[key]]
12
+ end
13
+
14
+ params.map { |p| p.map { |v| URI.escape(v.to_s, QUERY_STRING_RESERVERED) }.join("=") }.join("&")
15
+ end
16
+ end
@@ -0,0 +1,73 @@
1
+ require 'json'
2
+
3
+ module Withings::Api
4
+ # Class encapsulating a Measurement
5
+ #
6
+ # See www.withings.com/en/api/wbsapiv2
7
+ class Measurement
8
+ include ResultsHelpers
9
+
10
+ attr_accessor :measurement_type, :value_raw, :unit
11
+
12
+ def initialize(json_or_hash)
13
+ hash = coerce_hash json_or_hash
14
+
15
+ self.measurement_type = MeasurementType.lookup(hash["type"])
16
+ self.value_raw = hash["value"]
17
+ self.unit = hash["unit"]
18
+ end
19
+
20
+ def value
21
+ value_raw * 10**unit
22
+ end
23
+
24
+ end
25
+
26
+ # Class encapsulating a MeasurementGroup
27
+ #
28
+ # See www.withings.com/en/api/wbsapiv2
29
+ class MeasurementGroup
30
+ include ResultsHelpers
31
+
32
+ attr_reader :id, :attribution, :date_raw, :category, :measurements;
33
+
34
+ def initialize(json_string_or_hash)
35
+ hash = coerce_hash json_string_or_hash
36
+
37
+ #"grpid"=>2909, "attrib"=>0, "date"=>1222930968, "category"=>1, "measures
38
+ @id = hash["grpid"]
39
+ @date_raw = hash["date"]
40
+ @attribution = AttributionType.lookup(hash["attrib"])
41
+ @category = CategoryType.lookup(hash["category"])
42
+ @measurements = hash["measures"].map { |h| Measurement.new(h) }
43
+ end
44
+
45
+ def date
46
+ Time.at(date_raw)
47
+ end
48
+ end
49
+
50
+ # Class encapsulating the response to a call to
51
+ # measure/getmeas
52
+ #
53
+ # See www.withings.com/en/api/wbsapiv2
54
+ class MeasureGetmeasResults
55
+ include ResultsHelpers
56
+
57
+ attr_accessor :update_time_raw, :more, :measure_groups
58
+ alias :more? :more
59
+
60
+ def initialize(json_or_hash)
61
+ hash = coerce_hash json_or_hash
62
+
63
+ self.update_time_raw = hash["updatetime"] || raise(ArgumentError)
64
+ self.more = (hash["more"] == true)
65
+ self.measure_groups = hash["measuregrps"].map { |h| MeasurementGroup.new(h) }
66
+ end
67
+
68
+ def update_time
69
+ Time.at(update_time_raw)
70
+ end
71
+
72
+ end
73
+ end