tpitale/legato

View on GitHub
lib/legato/query.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module Legato
  class Query
    include Enumerable

    MONTH = 2592000
    REQUEST_FIELDS = 'columnHeaders/name,rows,totalResults,totalsForAllResults,containsSampledData'

    BASIC_OPTION_KEYS = [
      :sort, :limit, :offset, :start_date, :end_date, :quota_user,
      :user_ip, :sampling_level, :segment_id, :tracking_scope
    ]

    VALID_TRACKING_SCOPES = {
      'ga' => 'ga',
      'mcf' => 'mcf',
      'rt' => 'realtime'
    }

    def define_filter(name, &block)
      (class << self; self; end).instance_eval do
        define_method(name) {|*args| apply_filter(*args, &block)}
      end
    end

    def define_segment_filter(name, &block)
      (class << self; self; end).instance_eval do
        define_method(name) {|*args| apply_segment_filter(*args, &block)}
      end
    end

    def self.define_filter_operators(*methods)
      methods.each do |method|
        class_eval <<-CODE
          def #{method}(field, value, join_character=nil)
            Filter.new(self, field, :#{method}, value, join_character)
          end
        CODE
      end
    end

    attr_reader :parent_klass
    attr_accessor :profile, :start_date, :end_date
    attr_accessor :sort, :limit, :offset, :quota_user, :user_ip, :sampling_level, :segment_id #, :segment # individual, overwritten
    attr_accessor :filters, :segment_filters # combined, can be appended to
    attr_accessor :tracking_scope

    def self.from_query(query)
      new(query.parent_klass, query.tracking_scope, query.filters, query.segment_filters)
    end

    def initialize(klass, tracking_scope = "ga", filters = FilterSet.new, segment_filters = FilterSet.new)
      @loaded = false
      @parent_klass = klass
      self.filters = filters
      self.segment_filters = segment_filters
      self.start_date = Time.now - MONTH
      self.end_date = Time.now
      self.tracking_scope = tracking_scope

      klass.filters.each do |name, block|
        define_filter(name, &block)
      end

      klass.segments.each do |name, block|
        define_segment_filter(name, &block)
      end
    end

    def instance_klass
      @parent_klass.instance_klass
    end

    def apply_filter(*args, &block)
      apply_filter_expression(self.filters, *args, &block)
    end

    def apply_segment_filter(*args, &block)
      apply_filter_expression(self.segment_filters, *args, &block)
    end

    def apply_filter_expression(filter_set, *args, &block)
      # if given :filters or :segment_filters, make a set
      filter_set = send(filter_set) if filter_set.is_a?(Symbol)

      @profile = extract_profile(args)

      join_character = Legato.and_join_character # filters are joined by AND

      # # block returns one filter or an array of filters
      Array.wrap(instance_exec(*args, &block)).each do |filter|
        filter.join_character ||= join_character # only set when not set explicitly
        filter_set << filter

        join_character = Legato.or_join_character # arrays are joined by OR
      end
      self
    end

    def apply_options(options)
      if options.has_key?(:sort)
        # warn
        options[:sort] = options.delete(:sort)
      end

      if options.has_key?(:profile)
        self.profile = options.delete(:profile)
      end

      apply_basic_options(options)
      # apply_filter_options(options[:filters])

      self
    end

    def apply_basic_options(options)
      BASIC_OPTION_KEYS.each do |key| #:segment
        self.send("#{key}=".to_sym, options[key]) if options.has_key?(key)
      end
    end

    # return a hash of basic options to merge
    def basic_options
      Hash[BASIC_OPTION_KEYS.map { |k| [k, send(k)] }].reject {|_,v| v.nil?}
    end

    # def apply_filter_options(filter_options)
    #   join_character = Legato.and_join_character
    #
    #   Array.wrap(filter_options).compact.each do |filter|
    #     filter.each do |key, value|
    #       self.filters << hash_to_filter(key, value, join_character)
    #       join_character = Legato.and_join_character # hashes are joined by AND
    #     end
    #     join_character = Legato.or_join_character # arrays are joined by OR
    #   end
    # end

    # def hash_to_filter(key, value, join_character)
    #   field, operator = key, :eql
    #   field, operator = key.target, key.operator if key.is_a?(SymbolOperatorMethods)

    #   Filter.new(field, operator, value, join_character)
    # end

    def extract_profile(args)
      return args.shift if args.first.is_a?(Management::Profile)
      return args.pop if args.last.is_a?(Management::Profile)
      profile
    end

    define_filter_operators :eql, :not_eql, :gt, :gte, :lt, :lte, :matches,
      :does_not_match, :contains, :does_not_contain, :substring, :not_substring

    def loaded?
      @loaded
    end

    def load
      response = request_for_query
      @collection = response.collection
      @total_results = response.total_results
      @totals_for_all_results = response.totals_for_all_results
      @sampled = response.sampled
      @loaded = true
    end

    def collection
      load unless loaded?
      @collection
    end
    alias :to_a :collection

    def total_results
      load unless loaded?
      @total_results
    end

    def totals_for_all_results
      load unless loaded?
      @totals_for_all_results
    end

    def sampled
      load unless loaded?
      @sampled
    end

    def each(&block)
      collection.each(&block)
    end

    # if no filters, we use results to add profile
    def results(profile=nil, options={})
      query = loaded? ? Query.from_query(self) : self

      options, profile = profile, self.profile if profile.is_a?(Hash)

      query.profile = profile
      query.apply_options(self.basic_options.merge(options))
      query
    end

    # def total_results
    #   collection.total_results
    # end

    # def sampled?
    #   collection.sampled?
    # end

    def metrics
      @metrics ||= parent_klass.metrics.dup
    end

    def dimensions
      @dimensions ||= parent_klass.dimensions.dup
    end

    def sort=(arr)
      @sort = Legato::ListParameter.new(:sort, arr)
    end

    def segment
      "sessions::condition::#{segment_filters.to_params}" if segment_filters.any?
    end

    def segment_id=(segment_id)
      @segment_id = "gaid::#{segment_id}"
    end

    def profile_id
      profile && Legato.to_ga_string(profile.id)
    end

    def realtime?
      tracking_scope == 'rt'
    end

    def realtime
      self.tracking_scope = 'rt'
      self
    end

    def to_params
      base_params.tap do |params|

        [metrics, dimensions, sort].compact.each do |list|
          params.merge!(list.to_params(tracking_scope))
        end

      end
    end

    def to_query_string
      to_params.map {|k,v| [k,v].join("=")}.join("&")
    end

    def base_url
      raise "invalid tracking_scope" unless tracking_scope_valid?

      endpoint = VALID_TRACKING_SCOPES[tracking_scope]

      "https://www.googleapis.com/analytics/v3/data/#{endpoint}"
    end

    private

    def base_params
      {
        'ids' => profile_id,
        'start-date' => Legato.format_time(start_date),
        'end-date' => Legato.format_time(end_date),
        'max-results' => limit,
        'start-index' => offset,
        'segment' => segment_id || segment,
        'filters' => filters.to_params, # defaults to AND filtering
        'fields' => REQUEST_FIELDS,
        'quotaUser' => quota_user,
        'userIp' => user_ip,
        'samplingLevel' => sampling_level
      }.reject! {|_,v| v.nil? || v.to_s.strip.length == 0}
    end

    def tracking_scope_valid?
      VALID_TRACKING_SCOPES.keys.include?(tracking_scope)
    end

    def request_for_query
      profile.user.request(self)
    end
  end
end