From b78329b3f2310c8177eba3868ed0e2ef385f7e6e Mon Sep 17 00:00:00 2001 From: Zachery Moneypenny Date: Sun, 4 Mar 2012 13:59:45 -0600 Subject: [PATCH] New helper methods on Fitgem::Client + small fixes * Added label_for_measurements method to get the correct labels for each type of measurement with respect to the current user's settings on fitbit.com * Added connected? method to Fitgem::Client for an easy way to find out if the client has a valid access_token * Added several new error types used in helper methods * Bumped version to 0.5.2 * Fixed an issue in log_body_measurements where the date wasn't being run through the format_date helper * Added new specs for label_for_measurements method --- changelog.md | 34 +++++++++- lib/fitgem/body_measurements.rb | 8 ++- lib/fitgem/client.rb | 7 ++ lib/fitgem/errors.rb | 9 +++ lib/fitgem/helpers.rb | 117 ++++++++++++++++++++++++++++++++ lib/fitgem/version.rb | 2 +- spec/fitgem_helper_spec.rb | 83 ++++++++++++++++++++++ 7 files changed, 254 insertions(+), 6 deletions(-) diff --git a/changelog.md b/changelog.md index 91c33de..7b548fa 100644 --- a/changelog.md +++ b/changelog.md @@ -3,12 +3,42 @@ # fitgem changelog -## v0.4.0 +## v0.5.2 + +#### 2012-03-04 Zachery Moneypenny + +* Added new symbolize_keys helper method for turning the string-key based return hashes into symbol-key based ones +* Added new label_for_measurement helper method to get the correct unit measurement label given a measurement type and the current user's ApiUnitSystem setting +* Added specs +* Added new connected? method on Fitgem::Client that will report whether API calls may be made +* Added InvalidUnitSystem error and InvalidMeasurementType error +* Fixed a small issue where date values were not being formatted correctly in calls to log_body_measurements + +## v0.5.1 + +#### 2012-01-24 Zachery Moneypenny + +* Fix for creating and removing data subscriptions +* Updated specs + +## v0.5.0 + +#### 2012-01-22 Zachery Moneypenny + +* Added view/log/delete access for blood pressure data +* Added view/log/delete access for glucose data +* Added view/log/delete access for heart rate data +* Added updated time series documentation for new endpoints +* Updated temporal information in the readme +* Added unit tests for format_time method +* Updated copyright date + +## v0.4.0 #### 2011-11-29 Zachery Moneypenny * Added YARD documentation to thoroughly document code -* DEPRECATED: Fitgem::Client#log_weight method, use Fitgem::Client#log_body_measurements instead. +* DEPRECATED: Fitgem::Client#log_weight method, use Fitgem::Client#log_body_measurements instead. The new method allows you to log more than weight (bicep size, body fat %, etc.) * Added Fitgem::FoodFormType to be used in calls to Fitgem::Client#create_food * Added Fitgem::Client#log_sleep to log sleep data to fitbit diff --git a/lib/fitgem/body_measurements.rb b/lib/fitgem/body_measurements.rb index fbdb11b..7577790 100644 --- a/lib/fitgem/body_measurements.rb +++ b/lib/fitgem/body_measurements.rb @@ -25,7 +25,7 @@ def body_measurements_on_date(date) # an integer or a string in "X.XX'" format # @param [DateTime, String] date The date the weight should be # logged, as either a DateTime or a String in "yyyy-MM-dd" format - # @return [Hash] + # @return [Hash] # # @deprecated {#log_body_measurements} should be used instead of # log_weight @@ -35,7 +35,7 @@ def log_weight(weight, date, options={}) # Log body measurements to fitbit for the current user # - # At least ONE measurement item is REQUIRED in the call, as well as the + # At least ONE measurement item is REQUIRED in the call, as well as the # date. All measurement values to be logged for the user must be either an # Integer, a Decimal value, or a String in "X.XX" format. The # measurement units used for the supplied measurements are based on @@ -54,13 +54,15 @@ def log_weight(weight, date, options={}) # @option opts [Integer, Decimal, String] :bicep Bicep measurement # @option opts [DateTime, Date, String] :date Date to log measurements # for; provided either as a DateTime, Date, or a String in - # "yyyy-MM-dd" format + # "yyyy-MM-dd" format # # @return [Hash] Hash containing the key +:body+ with an inner hash # of all of the logged measurements # # @since v0.4.0 def log_body_measurements(opts) + # Update the date (if exists) + opts[:date] = format_date(opts[:date]) if opts[:date] post("/user/#{@user_id}/body.json", opts) end end diff --git a/lib/fitgem/client.rb b/lib/fitgem/client.rb index bd0dcef..4a3a29e 100644 --- a/lib/fitgem/client.rb +++ b/lib/fitgem/client.rb @@ -150,6 +150,13 @@ def reconnect(token, secret) access_token end + # Get the current state of the client + # + # @return True if api calls may be made, false if not + def connected? + !@access_token.nil? + end + # Get an oauth request token # # @param [Hash] opts Request token request data; can be used to diff --git a/lib/fitgem/errors.rb b/lib/fitgem/errors.rb index d249a8a..f30f09b 100644 --- a/lib/fitgem/errors.rb +++ b/lib/fitgem/errors.rb @@ -13,4 +13,13 @@ class InvalidTimeArgument < InvalidArgumentError class InvalidTimeRange < InvalidArgumentError end + + class InvalidUnitSystem < InvalidArgumentError + end + + class InvalidMeasurementType < InvalidArgumentError + end + + class ConnectionRequiredError < Exception + end end diff --git a/lib/fitgem/helpers.rb b/lib/fitgem/helpers.rb index d7975f6..6305c0c 100644 --- a/lib/fitgem/helpers.rb +++ b/lib/fitgem/helpers.rb @@ -63,5 +63,122 @@ def format_time(time) raise Fitgem::InvalidTimeArgument, "Date used must be a valid time object or a string in the format HH:mm; supplied argument is a #{time.class}" end end + + # Fetch the correct label for the desired measurement unit. + # + # The general use case for this method is that you are using the client for + # a specific user, and wish to get the correct labels for the unit measurements + # returned for that user. + # + # A secondary use case is that you wish to get the label for a measurement given a unit + # system that you supply (by setting the Fitgem::Client.api_unit_system attribute). + # + # In order for this method to get the correct value for the current user's preferences, + # the client must have the ability to make API calls. If you respect_user_unit_preferences + # is passed as 'true' (or left as the default value) and the client cannot make API calls + # then an error will be raised by the method. + # + # @param [Symbol] measurement_type The measurement type to fetch the label for + # @param [Boolean] respect_user_unit_preferences Should the method fetch the current user's + # specific measurement preferences and use those (true), or use the value set on Fitgem::Client.api_unit_system (false) + # @raise [Fitgem::ConnectionRequiredError] Raised when respect_user_unit_preferences is true but the + # client is not capable of making API calls. + # @raise [Fitgem::InvalidUnitSystem] Raised when the current value of Fitgem::Client.api_unit_system + # is not one of [ApiUnitSystem.US, ApiUnitSystem.UK, ApiUnitSystem.METRIC] + # @raise [Fitgem::InvalidMeasurementType] Raised when the supplied measurement_type is not one of + # [:duration, :distance, :elevation, :height, :weight, :measurements, :liquids, :blood_glucose] + # @return [String] The string label corresponding to the measurement type and + # current api_unit_system. + def label_for_measurement(measurement_type, respect_user_unit_preferences=true) + unless [:duration, :distance, :elevation, :height, :weight, :measurements, :liquids, :blood_glucose].include?(measurement_type) + raise InvalidMeasurementType, "Supplied measurement_type parameter must be one of [:duration, :distance, :elevation, :height, :weight, :measurements, :liquids, :blood_glucose], current value is :#{measurement_type}" + end + + selected_unit_system = api_unit_system + + if respect_user_unit_preferences + unless connected? + raise ConnectionRequiredError, "No connection to Fitbit API; one is required when passing respect_user_unit_preferences=true" + end + # Cache the unit systems for the current user + @unit_systems ||= self.user_info['user'].select {|key, value| key =~ /Unit$/ } + + case measurement_type + when :distance + selected_unit_system = @unit_systems["distanceUnit"] + when :height + selected_unit_system = @unit_systems["heightUnit"] + when :liquids + selected_unit_system = @unit_systems["waterUnit"] + when :weight + selected_unit_system = @unit_systems["weightUnit"] + when :blood_glucose + selected_unit_system = @unit_systems["glucoseUnit"] + else + selected_unit_system = api_unit_system + end + end + + # Fix the METRIC system difference + selected_unit_system = Fitgem::ApiUnitSystem.METRIC if selected_unit_system == "METRIC" + + # Ensure the target unit system is one that we know about + unless [ApiUnitSystem.US, ApiUnitSystem.UK, ApiUnitSystem.METRIC].include?(selected_unit_system) + raise InvalidUnitSystem, "The select unit system must be one of [ApiUnitSystem.US, ApiUnitSystem.UK, ApiUnitSystem.METRIC], current value is #{selected_unit_system}" + end + + unit_mappings[selected_unit_system][measurement_type] + end + + # Recursively turns arrays and hashes into symbol-key based + # structures. + # + # @param [Array, Hash] The structure to symbolize keys for + # @return A new structure with the keys symbolized + def self.symbolize_keys(obj) + case obj + when Array + obj.inject([]){|res, val| + res << case val + when Hash, Array + symbolize_keys(val) + else + val + end + res + } + when Hash + obj.inject({}){|res, (key, val)| + nkey = case key + when String + key.to_sym + else + key + end + nval = case val + when Hash, Array + symbolize_keys(val) + else + val + end + res[nkey] = nval + res + } + else + obj + end + end + + protected + + # Defined mappings for unit measurements to labels + def unit_mappings + { + ApiUnitSystem.US => { :duration => "milliseconds", :distance => "miles", :elevation => "feet", :height => "inches", :weight => "pounds", :measurements => "inches", :liquids => "fl oz", :blood_glucose => "mg/dL" }, + ApiUnitSystem.UK => { :duration => "milliseconds", :distance => "kilometers", :elevation => "meters", :height => "centimeters", :weight => "stone", :measurements => "centimeters", :liquids => "mL", :blood_glucose => "mmol/l" }, + ApiUnitSystem.METRIC => { :duration => "milliseconds", :distance => "kilometers", :elevation => "meters", :height => "centimeters", :weight => "kilograms", :measurements => "centimeters", :liquids => "mL", :blood_glucose => "mmol/l" } + } + end + end end diff --git a/lib/fitgem/version.rb b/lib/fitgem/version.rb index eed9e43..0d3863c 100644 --- a/lib/fitgem/version.rb +++ b/lib/fitgem/version.rb @@ -1,3 +1,3 @@ module Fitgem - VERSION = "0.5.1" + VERSION = "0.5.2" end diff --git a/spec/fitgem_helper_spec.rb b/spec/fitgem_helper_spec.rb index 390931d..607b477 100644 --- a/spec/fitgem_helper_spec.rb +++ b/spec/fitgem_helper_spec.rb @@ -124,4 +124,87 @@ }.to raise_error Fitgem::InvalidTimeArgument end end + + describe "#label_for_measurement" do + it "accepts the supported Fitgem::ApiUnitSystem values" do + @client.api_unit_system = Fitgem::ApiUnitSystem.US + @client.label_for_measurement :duration, false + @client.api_unit_system = Fitgem::ApiUnitSystem.UK + @client.label_for_measurement :duration, false + @client.api_unit_system = Fitgem::ApiUnitSystem.METRIC + @client.label_for_measurement :duration, false + end + + it "raises an InvalidUnitSystem error if the Fitgem::Client.api_unit_system value is invalid" do + expect { + @client.api_unit_system = "something else entirely" + @client.label_for_measurement :duration, false + }.to raise_error Fitgem::InvalidUnitSystem + end + + it "accepts the supported values for the measurement_type parameter" do + @client.label_for_measurement :duration, false + @client.label_for_measurement :distance, false + @client.label_for_measurement :elevation, false + @client.label_for_measurement :height, false + @client.label_for_measurement :weight, false + @client.label_for_measurement :measurements, false + @client.label_for_measurement :liquids, false + @client.label_for_measurement :blood_glucose, false + end + + it "raises an InvalidMeasurementType error if the measurement_type parameter is invalid" do + expect { + @client.label_for_measurement :homina, false + }.to raise_error Fitgem::InvalidMeasurementType + end + + it "returns the correct values when the unit system is Fitgem::ApiUnitSystem.US" do + @client.api_unit_system = Fitgem::ApiUnitSystem.US + @client.label_for_measurement(:duration, false).should == "milliseconds" + @client.label_for_measurement(:distance, false).should == "miles" + @client.label_for_measurement(:elevation, false).should == "feet" + @client.label_for_measurement(:height, false).should == "inches" + @client.label_for_measurement(:weight, false).should == "pounds" + @client.label_for_measurement(:measurements, false).should == "inches" + @client.label_for_measurement(:liquids, false).should == "fl oz" + @client.label_for_measurement(:blood_glucose, false).should == "mg/dL" + end + + it "returns the correct values when the unit system is Fitgem::ApiUnitSystem.UK" do + @client.api_unit_system = Fitgem::ApiUnitSystem.UK + @client.label_for_measurement(:duration, false).should == "milliseconds" + @client.label_for_measurement(:distance, false).should == "kilometers" + @client.label_for_measurement(:elevation, false).should == "meters" + @client.label_for_measurement(:height, false).should == "centimeters" + @client.label_for_measurement(:weight, false).should == "stone" + @client.label_for_measurement(:measurements, false).should == "centimeters" + @client.label_for_measurement(:liquids, false).should == "mL" + @client.label_for_measurement(:blood_glucose, false).should == "mmol/l" + end + + it "returns the correct values when the unit system is Fitgem::ApiUnitSystem.METRIC" do + @client.api_unit_system = Fitgem::ApiUnitSystem.METRIC + @client.label_for_measurement(:duration, false).should == "milliseconds" + @client.label_for_measurement(:distance, false).should == "kilometers" + @client.label_for_measurement(:elevation, false).should == "meters" + @client.label_for_measurement(:height, false).should == "centimeters" + @client.label_for_measurement(:weight, false).should == "kilograms" + @client.label_for_measurement(:measurements, false).should == "centimeters" + @client.label_for_measurement(:liquids, false).should == "mL" + @client.label_for_measurement(:blood_glucose, false).should == "mmol/l" + end + + context "when respecting the user's unit measurement preferences" do + before(:each) do + @client.stub(:connected?).and_return(true) + @client.stub(:user_info).and_return({"user" => {"distanceUnit"=>"en_GB", "glucoseUnit"=>"en_GB", "heightUnit"=>"en_GB", "waterUnit"=>"METRIC", "weightUnit"=>"en_GB"}}) + end + + it "returns the correct overridden measurement label" do + @client.api_unit_system = Fitgem::ApiUnitSystem.US + @client.label_for_measurement(:distance).should == "kilometers" + end + end + end end