lib/survey_gizmo/resource.rb in survey-gizmo-ruby-4.1.0 vs lib/survey_gizmo/resource.rb in survey-gizmo-ruby-5.0.2
- old
+ new
@@ -1,198 +1,194 @@
require 'set'
require 'addressable/uri'
module SurveyGizmo
+ class URLError < RuntimeError; end
+
module Resource
extend ActiveSupport::Concern
included do
include Virtus.model
- instance_variable_set('@paths', {})
+ instance_variable_set('@route', nil)
SurveyGizmo::Resource.descendants << self
end
- # @return [Set] Every class that includes SurveyGizmo::Resource
def self.descendants
@descendants ||= Set.new
end
# These are methods that every API resource can use to access resources in SurveyGizmo
module ClassMethods
- # Get an array of resources.
- # @param [Hash] options - simple URL params at the top level, and SurveyGizmo "filters" at the :filters key
+ attr_accessor :route
+
+ # Get an enumerator of resources.
+ # @param [Hash] conditions - URL and pagination params with SurveyGizmo "filters" at the :filters key
#
- # example: { page: 2, filters: [{ field: "istestdata", operator: "<>", value: 1 }] }
+ # Set all_pages: true if you want the gem to page through all the available responses
#
- # The top level keys (e.g. page, resultsperpage) get encoded in the url, while the
+ # example: { page: 2, filters: { field: "istestdata", operator: "<>", value: 1 } }
+ #
+ # The top level keys (e.g. :page, :resultsperpage) get encoded in the url, while the
# contents of the array of hashes passed at the :filters key get turned into the format
- # SurveyGizmo expects for its internal filtering, for example:
+ # SurveyGizmo expects for its internal filtering.
#
- # filter[field][0]=istestdata&filter[operator][0]=<>&filter[value][0]=1
- #
- # Set all_pages: true if you want the gem to page through all the available responses
- def all(conditions = {}, _deprecated_filters = {})
- conditions = merge_params(conditions, _deprecated_filters)
+ # Properties from the conditions hash (e.g. survey_id) will be added to the returned objects
+ def all(conditions = {})
fail ':all_pages and :page are mutually exclusive' if conditions[:page] && conditions[:all_pages]
+ logger.warn('WARNING: Only retrieving first page of results!') if conditions[:page].nil? && conditions[:all_pages].nil?
all_pages = conditions.delete(:all_pages)
- properties = conditions.dup
conditions[:resultsperpage] = SurveyGizmo.configuration.results_per_page unless conditions[:resultsperpage]
- request_route = handle_route!(:create, conditions)
- response = RestResponse.new(SurveyGizmo.get(request_route + filters_to_query_string(conditions)))
- collection = response.data.map { |datum| datum.is_a?(Hash) ? new(datum) : datum }
+ Enumerator.new do |yielder|
+ response = nil
- while all_pages && response.current_page < response.total_pages
- paged_filter = filters_to_query_string(conditions.merge(page: response.current_page + 1))
- response = RestResponse.new(SurveyGizmo.get(request_route + paged_filter))
- collection += response.data.map { |datum| datum.is_a?(Hash) ? new(datum) : datum }
- end
+ while !response || (all_pages && response['page'] < response['total_pages'])
+ conditions[:page] = response ? response['page'] + 1 : 1
+ logger.debug("Fetching #{name} page #{conditions} - #{conditions[:page]}#{response ? "/#{response['total_pages']}" : ''}...")
+ response = Connection.get(create_route(:create, conditions)).body
+ collection = response['data'].map { |datum| datum.is_a?(Hash) ? new(conditions.merge(datum)) : datum }
- # Add in the properties from the conditions hash because many of the important ones (like survey_id) are
- # not often part of the SurveyGizmo returned data
- properties.each do |k,v|
- if v && instance_methods.include?(k)
- collection.each { |c| c[k] ||= v }
+ # Sub questions are not pulled by default so we have to retrieve them manually. SurveyGizmo
+ # claims they will fix this bug and eventually all questions will be returned in one request.
+ if self == SurveyGizmo::API::Question
+ collection += collection.flat_map { |question| question.sub_questions }
+ end
+
+ collection.each { |e| yielder.yield(e) }
end
end
-
- # Sub questions are not pulled by default so we have to retrieve them manually
- # SurveyGizmo claims they will fix this bug and eventually all questions will be
- # returned in one request.
- if self == SurveyGizmo::API::Question
- collection += collection.map { |question| question.sub_questions }.flatten
- end
-
- collection
end
# Retrieve a single resource. See usage comment on .all
- def first(conditions, _deprecated_filters = {})
- conditions = merge_params(conditions, _deprecated_filters)
- properties = conditions.dup
-
- response = RestResponse.new(SurveyGizmo.get(handle_route!(:get, conditions) + filters_to_query_string(conditions)))
- # Add in the properties from the conditions hash because many of the important ones (like survey_id) are
- # not often part of the SurveyGizmo's returned data
- new(properties.merge(response.data))
+ def first(conditions = {})
+ new(conditions.merge(Connection.get(create_route(:get, conditions)).body['data']))
end
- # Create a new resource. Returns the newly created Resource instance.
+ # Create a new resource object locally and save to SurveyGizmo. Returns the newly created Resource instance.
def create(attributes = {})
- resource = new(attributes)
- resource.create_record_in_surveygizmo
- resource
+ new(attributes).save
end
# Delete resources
def destroy(conditions)
- RestResponse.new(SurveyGizmo.delete(handle_route!(:delete, conditions)))
+ Connection.delete(create_route(:delete, conditions))
end
- # Define the path where a resource is located
- def route(path, options)
- methods = options[:via]
- methods = [:get, :create, :update, :delete] if methods == :any
- methods.is_a?(Array) ? methods.each { |m| @paths[m] = path } : (@paths[methods] = path)
+ # @route is either a hash to be used directly or a string from which standard routes will be built
+ def routes
+ fail "route not set in #{name}" unless @route
+
+ return @route if @route.is_a?(Hash)
+ routes = { create: @route }
+ [:get, :update, :delete].each { |k| routes[k] = @route + '/:id' }
+ routes
end
- # Replaces the :page_id, :survey_id, etc strings defined in each model's URI routes with the
- # values being passed in interpolation hash with the same keys.
- #
- # This method has the SIDE EFFECT of deleting REST path related keys from interpolation_hash!
- def handle_route!(key, interpolation_hash)
- path = @paths[key]
- fail "No routes defined for `#{key}` in #{name}" unless path
- fail "User/password hash not setup!" if SurveyGizmo.default_params.empty?
+ # Replaces the :page_id, :survey_id, etc strings defined in each model's routes with the
+ # values in the params hash
+ def create_route(method, params)
+ fail "No route defined for #{method} on #{name}" unless routes[method]
- path.gsub(/:(\w+)/) do |m|
- raise(SurveyGizmo::URLError, "Missing RESTful parameters in request: `#{m}`") unless interpolation_hash[$1.to_sym]
- interpolation_hash.delete($1.to_sym)
+ url_params = params.dup
+ rest_path = routes[method].gsub(/:(\w+)/) do |m|
+ fail SurveyGizmo::URLError, "Missing RESTful parameters in request: `#{m}`" unless url_params[$1.to_sym]
+ url_params.delete($1.to_sym)
end
+
+ SurveyGizmo.configuration.api_version + rest_path + filters_to_query_string(url_params)
end
private
# Convert a [Hash] of params and internal surveygizmo style filters into a query string
- def filters_to_query_string(filters = {})
- return '' unless filters && filters.size > 0
+ #
+ # The hashes at the :filters key get turned into URL params like:
+ # # filter[field][0]=istestdata&filter[operator][0]=<>&filter[value][0]=1
+ def filters_to_query_string(params = {})
+ return '' unless params && params.size > 0
- params = {}
- (filters.delete(:filters) || []).each_with_index do |filter, i|
+ params = params.dup
+ url_params = {}
+
+ Array.wrap(params.delete(:filters)).each_with_index do |filter, i|
fail "Bad filter params: #{filter}" unless filter.is_a?(Hash) && [:field, :operator, :value].all? { |k| filter[k] }
- params["filter[field][#{i}]".to_sym] = "#{filter[:field]}"
- params["filter[operator][#{i}]".to_sym] = "#{filter[:operator]}"
- params["filter[value][#{i}]".to_sym] = "#{filter[:value]}"
+ url_params["filter[field][#{i}]".to_sym] = "#{filter[:field]}"
+ url_params["filter[operator][#{i}]".to_sym] = "#{filter[:operator]}"
+ url_params["filter[value][#{i}]".to_sym] = "#{filter[:value]}"
end
- uri = Addressable::URI.new
- uri.query_values = params.merge(filters)
+ uri = Addressable::URI.new(query_values: url_params.merge(params))
"?#{uri.query}"
end
- def merge_params(conditions, _deprecated_filters)
- $stderr.puts('Use of the 2nd hash parameter is deprecated.') unless _deprecated_filters.empty?
- conditions.merge(_deprecated_filters || {})
+ def logger
+ SurveyGizmo.configuration.logger
end
end
- # Save the resource to SurveyGizmo
+ ### BELOW HERE ARE INSTANCE METHODS ###
+
+ # If we have an id, it's an update, because we already know the surveygizmo assigned id
+ # Returns itself if successfully saved, but with attributes (like id) added by SurveyGizmo
def save
- if id
- # Then it's an update, because we already know the surveygizmo assigned id
- RestResponse.new(SurveyGizmo.post(handle_route(:update), query: attributes_without_blanks))
- else
- create_record_in_surveygizmo
- end
+ method, path = id ? [:post, :update] : [:put, :create]
+ self.attributes = Connection.send(method, create_route(path), attributes_without_blanks).body['data']
+ self
end
# Repopulate the attributes based on what is on SurveyGizmo's servers
def reload
- self.attributes = RestResponse.new(SurveyGizmo.get(handle_route(:get))).data
+ self.attributes = Connection.get(create_route(:get)).body['data']
self
end
# Delete the Resource from Survey Gizmo
def destroy
fail "No id; can't delete #{self.inspect}!" unless id
- RestResponse.new(SurveyGizmo.delete(handle_route(:delete)))
+ Connection.delete(create_route(:delete))
end
- # Sets the hash that will be used to interpolate values in routes. It needs to be defined per model.
- # @return [Hash] a hash of the values needed in routing
- def to_param_options
- fail "Define #to_param_options in #{self.class.name}"
- end
-
- # Returns itself if successfully saved, but with attributes added by SurveyGizmo
- def create_record_in_surveygizmo(attributes = {})
- rest_response = RestResponse.new(SurveyGizmo.put(handle_route(:create), query: attributes_without_blanks))
- self.attributes = rest_response.data
- self
- end
-
def inspect
attribute_strings = self.class.attribute_set.map do |attrib|
value = self.send(attrib.name)
value = value.is_a?(Hash) ? value.inspect : value.to_s
-
" \"#{attrib.name}\" => \"#{value}\"\n" unless value.strip.blank?
end.compact
"#<#{self.class.name}:#{self.object_id}>\n#{attribute_strings.join}"
end
- protected
+ private
def attributes_without_blanks
attributes.reject { |k,v| v.blank? }
end
- private
+ # Extract attributes required for API calls about this object
+ def route_params
+ params = { id: id }
- def handle_route(key)
- self.class.handle_route!(key, to_param_options)
+ self.class.routes.values.each do |route|
+ route.gsub(/:(\w+)/) do |m|
+ m = m.delete(':').to_sym
+ params[m] = self.send(m)
+ end
+ end
+
+ params
+ end
+
+ # Attributes that should be passed down the object hierarchy - e.g. a Question should have a survey_id
+ # Also used for loading member objects, e.g. loading Options for a given Question.
+ def children_params
+ klass_id = self.class.name.split('::').last.downcase + '_id'
+ route_params.merge(klass_id.to_sym => id).reject { |k,v| k == :id }
+ end
+
+ def create_route(method)
+ self.class.create_route(method, route_params)
end
end
end