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