require "set" require "addressable/uri" module SurveyGizmo module Resource extend ActiveSupport::Concern included do include Virtus.model instance_variable_set('@paths', {}) instance_variable_set('@collections', {}) 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 has to access resources # in Survey Gizmo module ClassMethods # Convert a [Hash] of filters into a query string # @param [Hash] filters # @return [String] def convert_filters_into_query_string(filters = nil) "" unless filters && filters.size > 0 uri = Addressable::URI.new uri.query_values = filters "?#{uri.query}" end # Get a list of resources # @param [Hash] conditions # @param [Hash] filters # @return [SurveyGizmo::Collection, Array] def all(conditions = {}, filters = nil) response = Response.new SurveyGizmo.get(handle_route(:create, conditions) + convert_filters_into_query_string(filters)) if response.ok? _collection = SurveyGizmo::Collection.new(self, nil, response.data) _collection.send(:options=, {:target => self, :parent => self}) _collection else [] end end # Get the first resource # @param [Hash] conditions # @param [Hash] filters # @return [Object, nil] def first(conditions = {}, filters = nil) response = Response.new SurveyGizmo.get(handle_route(:get, conditions) + convert_filters_into_query_string(filters)) response.ok? ? load(conditions.merge(response.data)) : nil end # Create a new resource # @param [Hash] attributes # @return [Resource] # The newly created Resource instance def create(attributes = {}) resource = new(attributes) resource.__send__(:_create) resource end # Copy a resource # @param [Integer] id # @param [Hash] attributes # @return [Resource] # The newly created resource instance def copy(attributes = {}) attributes[:copy] = true resource = new(attributes) resource.__send__(:_copy) resource end # Deleted the Resource from Survey Gizmo # @param [Hash] conditions # @return [Boolean] def destroy(conditions) response = Response.new SurveyGizmo.delete(handle_route(:delete, conditions)) response.ok? end # Define the path where a resource is located # @param [String] path # the path in Survey Gizmo for the resource # @param [Hash] options # @option options [Array] :via # which is `:get`, `:create`, `:update`, `:delete`, or `:any` # @scope class 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) nil end # @api private def load(attributes = {}) resource = new(attributes) resource.__send__(:clean!) resource end # Defines a new collection. These are child objects of the resource. # @macro [new] collection # @param [Symbol] resource_name the name of the collection, pluralized # @param [Class] model and optional class name if the class name does not match the resource_name # @return [Collection] # the $1 collection # @scope instance def collection(resource_name, model = nil) @collections[resource_name] = {:parent => self, :target => (model ? model : resource_name)} # workaround for weird bug with passing a class to Collection class_eval(<<-EOS) def #{resource_name} @#{resource_name} ||= [] end def #{resource_name}=(array) @#{resource_name} = SurveyGizmo::Collection.new(#{self}, :#{resource_name}, array) end EOS end # @api private def collections @collections.dup.freeze end # @api private def handle_route(key, *interp) path = @paths[key] raise "No routes defined for `#{key}` in #{self.name}" unless path options = interp.last.is_a?(Hash) ? interp.pop : path.scan(/:(\w+)/).inject({}){|hash, k| hash.merge(k.to_sym => interp.shift) } path.gsub(/:(\w+)/) do |m| options[$1.to_sym].tap{ |result| raise(SurveyGizmo::URLError, "Missing parameters in request: `#{m}`") unless result } end end end # Updates attributes and saves this Resource instance # # @param [Hash] attributes # attributes to be updated # # @return [Boolean] # true if resource is saved def update(attributes = {}) self.attributes = attributes self.save end # Save the instance to Survey Gizmo # # @return [Boolean] # true if Resource instance is saved def save if new? _create else handle_response SurveyGizmo.post(handle_route(:update), :query => self.attributes_without_blanks) do _response.ok? ? saved! : false end end end # fetch resource from SurveyGizmo and reload the attributes # @return [self, false] # Returns the object, if saved. Otherwise returns false. def reload handle_response SurveyGizmo.get(handle_route(:get)) do if _response.ok? self.attributes = _response.data clean! else false end end end # Deleted the Resource from Survey Gizmo # @return [Boolean] def destroy return false if new? || destroyed? handle_response SurveyGizmo.delete(handle_route(:delete)) do _response.ok? ? destroyed! : false end end # The state of the current Resource # @api private def new? @_state.nil? end # @todo This seemed like a good way to prevent accidently trying to perform an action # on a record at a point when it would fail. Not sure if it's really necessary though. [:clean, # stored and not dirty :saved, # stored and not modified :destroyed, # duh! :zombie # needs to be stored ].each do |state| # Change the method state to $1 define_method("#{state}!") do @_state = state true end # Inquire about the method state if $1 define_method("#{state}?") do @_state == state end private "#{state}!" 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 raise "Define #to_param_options in #{self.class.name}" end # Any errors returned by Survey Gizmo # @return [Array] def errors @errors ||= [] end # @return [Hash] The raw JSON returned by Survey Gizmo def raw_response _response.response if _response end # @visibility private def inspect attrs = self.class.attribute_set.map do |attrib| value = attrib.get!(self).inspect "@#{attrib.name}=#{value}" if attrib.respond_to?(:name) end "#<#{self.class.name}:#{self.object_id} #{attrs.join(' ')}>" end # This class normalizes the response returned by Survey Gizmo class Response def ok? @response && @response['result_ok'] end # The parsed JSON data of the response def data @_data ||= (@response['data'] || {}) end # The error message if there is one def message @_message ||= @response['message'] end attr_reader :response private def cleanup_attribute_name(attr) attr.downcase.gsub(/[^[:alnum:]]+/,'_').gsub(/(url|variable|standard|shown)/,'').gsub(/_+/,'_').gsub(/^_/,'').gsub(/_$/,'') end def find_attribute_parent(attr) case attr.downcase when /url/ "url" when /variable.*standard/ "meta" when /variable.*shown/ "shown" when /variable/ "variable" when /question/ "answers" end end def initialize(response) @response = response.parsed_response return if @response.nil? or not ok? @_data = @response['data'] # Handle really crappy [] notation in SG API, so far just in SurveyResponse items = (@_data.is_a?(Array) ? @_data : [@_data]).compact items.each do |data_item| data_item.keys.grep(/^\[/).each do |key| next if data_item[key].nil? || data_item[key].length == 0 parent = find_attribute_parent(key) data_item[parent] = {} unless data_item[parent] case key.downcase when /(url|variable.*standard)/ data_item[parent][cleanup_attribute_name(key).to_sym] = data_item[key] when /variable.*shown/ data_item[parent][cleanup_attribute_name(key).to_i] = data_item[key].include?("1") when /variable/ data_item[parent][cleanup_attribute_name(key).to_i] = data_item[key].to_i when /question/ data_item[parent][key] = data_item[key] end data_item.delete(key) end end unless items.blank? end end protected def attributes_without_blanks self.attributes.reject{|k,v| v.blank? } end private # The response object from SurveyGizmo. Useful for viewing the raw data returned attr_reader :_response def set_response(http) @_response = Response.new(http) end def handle_route(key) self.class.handle_route(key, to_param_options) end def handle_response(resp, &block) set_response(resp) (self.errors << _response.message) unless _response.ok? self.errors.clear if !self.errors.empty? && _response.ok? instance_eval(&block) end def _create(attributes = {}) http = SurveyGizmo.put(handle_route(:create), :query => self.attributes_without_blanks) handle_response http do if _response.ok? self.attributes = _response.data saved! else false end end end def _copy(attributes = {}) http = SurveyGizmo.post(handle_route(:update), :query => self.attributes_without_blanks) handle_response http do if _response.ok? self.attributes = _response.data saved! else false end end end end end